From c684c3996842ac8b383af6b90be229de92160c3a Mon Sep 17 00:00:00 2001 From: Dmytro Khrysiev Date: Fri, 27 Feb 2026 16:40:21 +0100 Subject: [PATCH 01/23] feat(orocommerce): allow to install activepieces to subfolder of the project --- docker-entrypoint.sh | 6 +- packages/react-ui/index.html | 8 +- packages/react-ui/src/app/guards/index.tsx | 9 +- .../src/app/routes/embed-ce/index.tsx | 180 ++++++++++++++++++ .../src/components/socket-provider.tsx | 2 +- .../components/third-party-logins.tsx | 4 +- packages/react-ui/src/i18n.ts | 3 + packages/react-ui/src/lib/api.ts | 2 +- .../react-ui/src/lib/navigation-utils.tsx | 11 +- packages/react-ui/vite-plugins/html-plugin.js | 16 +- packages/react-ui/vite.config.mts | 14 +- 11 files changed, 231 insertions(+), 24 deletions(-) create mode 100644 packages/react-ui/src/app/routes/embed-ce/index.tsx diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh index 5d3855d92f15..6f4c1f61662b 100644 --- a/docker-entrypoint.sh +++ b/docker-entrypoint.sh @@ -3,13 +3,15 @@ # Set default values if not provided export AP_APP_TITLE="${AP_APP_TITLE:-Activepieces}" export AP_FAVICON_URL="${AP_FAVICON_URL:-https://cdn.activepieces.com/brand/favicon.ico}" +export AP_ASSETS_PREFIX="${AP_ASSETS_PREFIX:-}" # Debug: Print environment variables echo "AP_APP_TITLE: $AP_APP_TITLE" echo "AP_FAVICON_URL: $AP_FAVICON_URL" +echo "AP_ASSETS_PREFIX: $AP_ASSETS_PREFIX" # Process environment variables in index.html BEFORE starting services -envsubst '${AP_APP_TITLE} ${AP_FAVICON_URL}' < /usr/share/nginx/html/index.html > /usr/share/nginx/html/index.html.tmp && \ +envsubst '${AP_APP_TITLE} ${AP_FAVICON_URL} ${AP_ASSETS_PREFIX}' < /usr/share/nginx/html/index.html > /usr/share/nginx/html/index.html.tmp && \ mv /usr/share/nginx/html/index.html.tmp /usr/share/nginx/html/index.html @@ -23,4 +25,4 @@ if [ "$AP_CONTAINER_TYPE" = "APP" ] && [ "$AP_PM2_ENABLED" = "true" ]; then else echo "Starting backend server with Node.js (WORKER mode or default)" node --enable-source-maps dist/packages/server/api/main.cjs -fi \ No newline at end of file +fi diff --git a/packages/react-ui/index.html b/packages/react-ui/index.html index aa3c0439eb8a..333db64f2c27 100644 --- a/packages/react-ui/index.html +++ b/packages/react-ui/index.html @@ -4,18 +4,18 @@ - + <%= apTitle %> - +
- + - \ No newline at end of file + diff --git a/packages/react-ui/src/app/guards/index.tsx b/packages/react-ui/src/app/guards/index.tsx index 0c5929dd9297..a15a57f3b048 100644 --- a/packages/react-ui/src/app/guards/index.tsx +++ b/packages/react-ui/src/app/guards/index.tsx @@ -9,6 +9,7 @@ import { import { PageTitle } from '@/app/components/page-title'; import { ChatPage } from '@/app/routes/chat'; import { EmbedPage } from '@/app/routes/embed'; +import { EmbedCePage } from '@/app/routes/embed-ce'; import AnalyticsPage from '@/app/routes/impact'; import { ApiKeysPage } from '@/app/routes/platform/security/api-keys'; import { SigningKeysPage } from '@/app/routes/platform/security/signing-keys'; @@ -91,6 +92,10 @@ const routes = [ path: '/embed/connections', element: , }, + { + path: '/embed-ce', + element: , + }, { path: '/authenticate', element: , @@ -601,7 +606,9 @@ const routes = [ ]; export const memoryRouter = createMemoryRouter(routes); -const browserRouter = createBrowserRouter(routes); +const browserRouter = createBrowserRouter(routes, { + basename: import.meta.env.BASE_URL, +}); const ApRouter = () => { const { embedState } = useEmbedding(); diff --git a/packages/react-ui/src/app/routes/embed-ce/index.tsx b/packages/react-ui/src/app/routes/embed-ce/index.tsx new file mode 100644 index 000000000000..6b5687905c2a --- /dev/null +++ b/packages/react-ui/src/app/routes/embed-ce/index.tsx @@ -0,0 +1,180 @@ +import React from 'react'; +import { flushSync } from 'react-dom'; +import { useTranslation } from 'react-i18next'; +import { useEffectOnce } from 'react-use'; + +import { memoryRouter } from '@/app/guards'; +import { useEmbedding } from '@/components/embed-provider'; +import { useTheme } from '@/components/theme-provider'; +import { LoadingScreen } from '@/components/ui/loading-screen'; +import { useAuthorization } from '@/hooks/authorization-hooks'; +import { + combinePaths, + determineDefaultRoute, + parentWindow, + routesThatRequireProjectId, +} from '@/lib/utils'; +import { + ActivepiecesClientAuthenticationSuccess, + ActivepiecesClientConfigurationFinished, + ActivepiecesClientEventName, + ActivepiecesClientInit, + ActivepiecesVendorEventName, + ActivepiecesVendorInit, + ActivepiecesVendorRouteChanged, +} from 'ee-embed-sdk'; + +// Copied from embed/index.tsx — notifies host that auth + config are done +const notifyVendorPostAuthentication = () => { + const authenticationSuccessEvent: ActivepiecesClientAuthenticationSuccess = { + type: ActivepiecesClientEventName.CLIENT_AUTHENTICATION_SUCCESS, + data: {}, + }; + parentWindow.postMessage(authenticationSuccessEvent, '*'); + const configurationFinishedEvent: ActivepiecesClientConfigurationFinished = { + type: ActivepiecesClientEventName.CLIENT_CONFIGURATION_FINISHED, + data: {}, + }; + parentWindow.postMessage(configurationFinishedEvent, '*'); +}; + +// Copied from embed/index.tsx — listens for VENDOR_ROUTE_CHANGED from host +const handleVendorNavigation = ({ projectId }: { projectId: string }) => { + const handleVendorRouteChange = ( + event: MessageEvent, + ) => { + if ( + event.source === parentWindow && + event.data.type === ActivepiecesVendorEventName.VENDOR_ROUTE_CHANGED + ) { + const targetRoute = event.data.data.vendorRoute; + const targetRouteRequiresProjectId = Object.values( + routesThatRequireProjectId, + ).some((route) => targetRoute.includes(route)); + if (!targetRouteRequiresProjectId) { + memoryRouter.navigate(targetRoute); + } else { + memoryRouter.navigate( + combinePaths({ + secondPath: targetRoute, + firstPath: `/projects/${projectId}`, + }), + ); + } + } + }; + window.addEventListener('message', handleVendorRouteChange); +}; + +// Copied from embed/index.tsx — posts CLIENT_ROUTE_CHANGED to host on navigation +const handleClientNavigation = () => { + memoryRouter.subscribe((state) => { + const pathNameWithoutProjectOrProjectId = state.location.pathname.replace( + /\/projects\/[^/]+/, + '', + ); + parentWindow.postMessage( + { + type: ActivepiecesClientEventName.CLIENT_ROUTE_CHANGED, + data: { + route: pathNameWithoutProjectOrProjectId + state.location.search, + }, + }, + '*', + ); + }); +}; + +const EmbedCePage = React.memo(() => { + const { setEmbedState, embedState } = useEmbedding(); + const { setTheme } = useTheme(); + const { i18n } = useTranslation(); + const { checkAccess } = useAuthorization(); + + const initState = (event: MessageEvent) => { + if ( + event.source !== parentWindow || + event.data.type !== ActivepiecesVendorEventName.VENDOR_INIT + ) { + return; + } + + // CE: no jwtToken exchange — token is already in localStorage from the + // parent app (same origin). We just read it directly. + const token = window.localStorage.getItem('token'); + const projectId = window.localStorage.getItem('projectId'); + + if (!token || !projectId) { + memoryRouter.navigate('/sign-in'); + return; + } + + if (event.data.data.mode) { + setTheme(event.data.data.mode); + } + + if (event.data.data.locale) { + i18n.changeLanguage(event.data.data.locale); + } + + const configuredRoute = event.data.data.initialRoute ?? '/'; + const defaultRoute = determineDefaultRoute(checkAccess); + const initialRoute = + configuredRoute === '/' ? defaultRoute : configuredRoute; + + // Must use flushSync so the router switches to memoryRouter before navigate, + // mirroring the same pattern used in the original EmbedPage. + flushSync(() => { + setEmbedState({ + isEmbedded: true, + hideSideNav: event.data.data.hideSidebar ?? true, + hideFlowsPageNavbar: event.data.data.hideFlowsPageNavbar ?? false, + disableNavigationInBuilder: + event.data.data.disableNavigationInBuilder !== false, + hideFolders: event.data.data.hideFolders ?? false, + hideFlowNameInBuilder: event.data.data.hideFlowNameInBuilder ?? false, + sdkVersion: event.data.data.sdkVersion, + fontUrl: event.data.data.fontUrl, + fontFamily: event.data.data.fontFamily, + useDarkBackground: false, + hideExportAndImportFlow: + event.data.data.hideExportAndImportFlow ?? false, + hideHomeButtonInBuilder: + event.data.data.disableNavigationInBuilder === 'keep_home_button_only' + ? false + : event.data.data.disableNavigationInBuilder, + emitHomeButtonClickedEvent: + event.data.data.emitHomeButtonClickedEvent ?? false, + homeButtonIcon: event.data.data.homeButtonIcon ?? 'logo', + hideDuplicateFlow: event.data.data.hideDuplicateFlow ?? false, + hidePageHeader: event.data.data.hidePageHeader ?? false, + }); + }); + + memoryRouter.navigate(initialRoute); + handleVendorNavigation({ projectId }); + handleClientNavigation(); + notifyVendorPostAuthentication(); + }; + + useEffectOnce(() => { + // Send CLIENT_INIT to signal the host that the iframe is ready. + // The host (JS component) should reply with VENDOR_INIT carrying config. + // If no VENDOR_INIT arrives within the listener lifetime, the page stays + // on the loading screen — the JS component must handle this. + const initEvent: ActivepiecesClientInit = { + type: ActivepiecesClientEventName.CLIENT_INIT, + data: {}, + }; + parentWindow.postMessage(initEvent, '*'); + window.addEventListener('message', initState); + return () => { + window.removeEventListener('message', initState); + }; + }); + + return ; +}); + +EmbedCePage.displayName = 'EmbedCePage'; +export { EmbedCePage }; diff --git a/packages/react-ui/src/components/socket-provider.tsx b/packages/react-ui/src/components/socket-provider.tsx index 06d8a6ff09be..36df3c9deab6 100644 --- a/packages/react-ui/src/components/socket-provider.tsx +++ b/packages/react-ui/src/components/socket-provider.tsx @@ -7,7 +7,7 @@ import { authenticationSession } from '@/lib/authentication-session'; const socket = io(API_BASE_URL, { transports: ['websocket'], - path: '/api/socket.io', + path: `${import.meta.env.BASE_URL}api/socket.io`, autoConnect: false, reconnection: true, }); diff --git a/packages/react-ui/src/features/authentication/components/third-party-logins.tsx b/packages/react-ui/src/features/authentication/components/third-party-logins.tsx index 8e0910addace..690bfe266d46 100644 --- a/packages/react-ui/src/features/authentication/components/third-party-logins.tsx +++ b/packages/react-ui/src/features/authentication/components/third-party-logins.tsx @@ -47,7 +47,9 @@ const ThirdPartyLogin = React.memo(({ isSignUp }: { isSignUp: boolean }) => { }; const signInWithSaml = () => - (window.location.href = '/api/v1/authn/saml/login'); + (window.location.href = `${ + import.meta.env.BASE_URL + }api/v1/authn/saml/login`); return (
diff --git a/packages/react-ui/src/i18n.ts b/packages/react-ui/src/i18n.ts index dc9e61eb7c10..7c397d3cde46 100644 --- a/packages/react-ui/src/i18n.ts +++ b/packages/react-ui/src/i18n.ts @@ -20,5 +20,8 @@ i18n keySeparator: false, nsSeparator: false, returnEmptyString: false, + backend: { + loadPath: `${import.meta.env.BASE_URL}locales/{{lng}}/{{ns}}.json`, + }, }); export default i18n; diff --git a/packages/react-ui/src/lib/api.ts b/packages/react-ui/src/lib/api.ts index e3eec56c02d9..7c4499f32bcf 100644 --- a/packages/react-ui/src/lib/api.ts +++ b/packages/react-ui/src/lib/api.ts @@ -14,7 +14,7 @@ export const API_BASE_URL = import.meta.env.MODE === 'cloud' ? 'https://cloud.activepieces.com' : window.location.origin; -export const API_URL = `${API_BASE_URL}/api`; +export const API_URL = `${API_BASE_URL}${import.meta.env.BASE_URL}api`; const disallowedRoutes = [ '/v1/managed-authn/external-token', diff --git a/packages/react-ui/src/lib/navigation-utils.tsx b/packages/react-ui/src/lib/navigation-utils.tsx index 6c4c2afdd5b6..c8dac4addfe3 100644 --- a/packages/react-ui/src/lib/navigation-utils.tsx +++ b/packages/react-ui/src/lib/navigation-utils.tsx @@ -12,12 +12,11 @@ export const useNewWindow = () => { search: searchParams, }); } else { - return (route: string, searchParams?: string) => - window.open( - `${route}${searchParams ? '?' + searchParams : ''}`, - '_blank', - 'noopener noreferrer', - ); + return (route: string, searchParams?: string) => { + const base = import.meta.env.BASE_URL.replace(/\/$/, ''); + const url = `${base}${route}${searchParams ? '?' + searchParams : ''}`; + window.open(url, '_blank', 'noopener noreferrer'); + }; } }; diff --git a/packages/react-ui/vite-plugins/html-plugin.js b/packages/react-ui/vite-plugins/html-plugin.js index 26005d38385a..27248a83a0ce 100644 --- a/packages/react-ui/vite-plugins/html-plugin.js +++ b/packages/react-ui/vite-plugins/html-plugin.js @@ -3,6 +3,7 @@ * @typedef {Object} CustomHtmlPluginOptions * @property {string} title - The title to be injected into the HTML. * @property {string} icon - The icon URL to be set as the favicon. + * @property {string} base - The full base path (e.g. /admin/activepieces-instance/) */ /** @@ -11,12 +12,17 @@ export default function customHtmlPlugin(options) { return { name: 'custom-html', - transformIndexHtml(html) { + transformIndexHtml: { + order: 'pre', + handler(html) { let newHtml = html.replace(/<%= apTitle %>/g, options.title || ''); - newHtml = newHtml.replace(/<%= apFavicon %>/g, options.icon || ''); - + newHtml = newHtml.replace( + /<%= apBase %>/g, + options.base || '/' + ); return newHtml; }, - }; - } \ No newline at end of file + }, + }; + } diff --git a/packages/react-ui/vite.config.mts b/packages/react-ui/vite.config.mts index ddcfa759622b..0fc279052407 100644 --- a/packages/react-ui/vite.config.mts +++ b/packages/react-ui/vite.config.mts @@ -17,17 +17,24 @@ export default defineConfig(({ command, mode }) => { ? 'https://activepieces.com/favicon.ico' : '${AP_FAVICON_URL}'; + const AP_ASSETS_PREFIX = isDev + ? process.env.AP_ASSETS_PREFIX ?? '' + : '${AP_ASSETS_PREFIX}'; + + const base = AP_ASSETS_PREFIX ? `/${AP_ASSETS_PREFIX}/` : '/'; + return { + base, root: __dirname, cacheDir: '../../node_modules/.vite/packages/react-ui', server: { - // allowedHosts: ['wozcsvaint.loclx.io'], + allowedHosts: ['commerce-crm-ee.master.loc'], proxy: { - '/api': { + [`${base}api`]: { target: 'http://127.0.0.1:3000', secure: false, changeOrigin: true, - rewrite: (path) => path.replace(/^\/api/, ''), + rewrite: (path) => path.replace(new RegExp(`^${base}api`), ''), headers: { Host: '127.0.0.1:4200', }, @@ -70,6 +77,7 @@ export default defineConfig(({ command, mode }) => { customHtmlPlugin({ title: AP_TITLE, icon: AP_FAVICON, + base, }), checker({ typescript: { From ba3aeb5937e90b06213c1c37b358c73a31495b7b Mon Sep 17 00:00:00 2001 From: Dmytro Khrysiev Date: Wed, 11 Mar 2026 14:11:18 +0100 Subject: [PATCH 02/23] feat(orocommerce): subfolder-install fixes for 0.79+ --- packages/web/src/app/routes/embed-ce/index.tsx | 9 ++++++--- packages/web/src/lib/authentication-session.ts | 4 ++-- packages/web/src/lib/navigation-utils.tsx | 5 +++-- packages/web/vite.config.mts | 7 +++++-- 4 files changed, 16 insertions(+), 9 deletions(-) diff --git a/packages/web/src/app/routes/embed-ce/index.tsx b/packages/web/src/app/routes/embed-ce/index.tsx index 15c25a978fe0..d118e89688e5 100644 --- a/packages/web/src/app/routes/embed-ce/index.tsx +++ b/packages/web/src/app/routes/embed-ce/index.tsx @@ -108,13 +108,16 @@ const EmbedCePage = React.memo(() => { return; } + // Notify listeners (e.g. telemetry-provider) that auth state is available, + // mirroring the window.dispatchEvent(new Event('storage')) call inside + // authenticationSession.saveResponse that EmbedPage triggers. + window.dispatchEvent(new Event('storage')); + if (event.data.data.mode) { setTheme(event.data.data.mode); } - if (event.data.data.locale) { - i18n.changeLanguage(event.data.data.locale); - } + i18n.changeLanguage(event.data.data.locale ?? 'en'); const configuredRoute = event.data.data.initialRoute ?? '/'; const defaultRoute = determineDefaultRoute(checkAccess); diff --git a/packages/web/src/lib/authentication-session.ts b/packages/web/src/lib/authentication-session.ts index 959e18ff8d64..4cf779b47b31 100644 --- a/packages/web/src/lib/authentication-session.ts +++ b/packages/web/src/lib/authentication-session.ts @@ -89,7 +89,7 @@ export const authenticationSession = { }); ApStorage.getInstance().setItem(tokenKey, result.token); ApStorage.getInstance().setItem(projectIdKey, result.projectId); - window.location.href = '/'; + window.location.href = import.meta.env.BASE_URL; }, switchToProject(projectId: string) { if (authenticationSession.getProjectId() === projectId) { @@ -111,7 +111,7 @@ export const authenticationSession = { }, logOut() { this.clearSession(); - window.location.href = '/sign-in'; + window.location.href = `${import.meta.env.BASE_URL}sign-in`; }, }; diff --git a/packages/web/src/lib/navigation-utils.tsx b/packages/web/src/lib/navigation-utils.tsx index 18f8e92eff2c..23fef18bf768 100644 --- a/packages/web/src/lib/navigation-utils.tsx +++ b/packages/web/src/lib/navigation-utils.tsx @@ -13,8 +13,9 @@ export const useNewWindow = () => { }); } else { return (route: string, searchParams?: string) => { - const base = import.meta.env.BASE_URL.replace(/\/$/, ''); - const url = `${base}${route}${searchParams ? '?' + searchParams : ''}`; + const url = `${import.meta.env.BASE_URL}${route.replace(/^\//, '')}${ + searchParams ? '?' + searchParams : '' + }`; window.open(url, '_blank', 'noopener noreferrer'); }; } diff --git a/packages/web/vite.config.mts b/packages/web/vite.config.mts index 1d184b4a7d21..ceaafb3b5bb2 100644 --- a/packages/web/vite.config.mts +++ b/packages/web/vite.config.mts @@ -3,7 +3,7 @@ import path from 'path'; import tsconfigPaths from 'vite-tsconfig-paths'; import react from '@vitejs/plugin-react'; -import { defineConfig } from 'vite'; +import { defineConfig, loadEnv } from 'vite'; import checker from 'vite-plugin-checker'; import tailwindcss from '@tailwindcss/vite'; import customHtmlPlugin from './vite-plugins/html-plugin'; @@ -11,6 +11,9 @@ import customHtmlPlugin from './vite-plugins/html-plugin'; export default defineConfig(({ command, mode }) => { const isDev = command === 'serve' || mode === 'development'; + // Load env vars from the monorepo root .env file (not just shell environment) + const rootEnv = isDev ? loadEnv(mode, path.resolve(__dirname, '../..'), '') : {}; + const AP_TITLE = isDev ? 'Activepieces' : '${AP_APP_TITLE}'; const AP_FAVICON = isDev @@ -18,7 +21,7 @@ export default defineConfig(({ command, mode }) => { : '${AP_FAVICON_URL}'; const AP_ASSETS_PREFIX = isDev - ? process.env.AP_ASSETS_PREFIX ?? '' + ? process.env.AP_ASSETS_PREFIX ?? rootEnv.AP_ASSETS_PREFIX ?? '' : '${AP_ASSETS_PREFIX}'; const base = AP_ASSETS_PREFIX ? `/${AP_ASSETS_PREFIX}/` : '/'; From ad4057b2b1d378809f2588a11a850212ab858ad4 Mon Sep 17 00:00:00 2001 From: Dmytro Khrysiev Date: Thu, 19 Mar 2026 14:52:23 +0100 Subject: [PATCH 03/23] feat(orocommerce): subfolder installation fixes --- packages/web/vite.config.mts | 34 +++++++++++++++++++++++----------- 1 file changed, 23 insertions(+), 11 deletions(-) diff --git a/packages/web/vite.config.mts b/packages/web/vite.config.mts index 234bf4b9c746..f0f72d4a45c2 100644 --- a/packages/web/vite.config.mts +++ b/packages/web/vite.config.mts @@ -1,9 +1,12 @@ /// import path from 'path'; +// CUSTOMIZATION START: embedding >> +import donenv from 'dotenv'; +// << CUSTOMIZATION END: embedding import tsconfigPaths from 'vite-tsconfig-paths'; import react from '@vitejs/plugin-react'; -import { defineConfig, loadEnv } from 'vite'; +import { defineConfig } from 'vite'; import checker from 'vite-plugin-checker'; import tailwindcss from '@tailwindcss/vite'; import customHtmlPlugin from './vite-plugins/html-plugin'; @@ -11,30 +14,37 @@ import customHtmlPlugin from './vite-plugins/html-plugin'; export default defineConfig(({ command, mode }) => { const isDev = command === 'serve' || mode === 'development'; - // Load env vars from the monorepo root .env file (not just shell environment) - const rootEnv = isDev ? loadEnv(mode, path.resolve(__dirname, '../..'), '') : {}; - const AP_TITLE = 'Activepieces'; const AP_FAVICON = 'https://activepieces.com/favicon.ico'; - const AP_ASSETS_PREFIX = isDev - ? process.env.AP_ASSETS_PREFIX ?? rootEnv.AP_ASSETS_PREFIX ?? '' - : '${AP_ASSETS_PREFIX}'; - - const base = AP_ASSETS_PREFIX ? `/${AP_ASSETS_PREFIX}/` : '/'; + // CUSTOMIZATION START: embedding >> + // TODO: we will need to support real .env files loading? + donenv.config({ path: path.resolve(__dirname, '../../.env.dev') }); + let base = '/'; + const allowedHosts = []; + if (process.env.AP_FRONTEND_URL) { + const AP_FRONTEND_URL = new URL(process.env.AP_FRONTEND_URL); + const AP_ASSETS_PREFIX = AP_FRONTEND_URL.pathname.replace(/^\/|\/$/, ''); + allowedHosts.push(AP_FRONTEND_URL.host); + base = `/${AP_ASSETS_PREFIX}/`; + } + // << CUSTOMIZATION END: embedding return { base, root: __dirname, cacheDir: '../../node_modules/.vite/packages/web', server: { - allowedHosts: ['commerce-crm-ee.master.loc'], + // CUSTOMIZATION START: embedding >> + allowedHosts: allowedHosts, + // << CUSTOMIZATION END: embedding proxy: { + // CUSTOMIZATION START: embedding >> [`${base}api`]: { + // << CUSTOMIZATION END: embedding target: 'http://127.0.0.1:3000', secure: false, changeOrigin: true, - rewrite: (path) => path.replace(new RegExp(`^${base}api`), ''), headers: { Host: '127.0.0.1:4200', }, @@ -73,7 +83,9 @@ export default defineConfig(({ command, mode }) => { customHtmlPlugin({ title: AP_TITLE, icon: AP_FAVICON, + // CUSTOMIZATION START: embedding >> base, + // << CUSTOMIZATION END: embedding }), ...(isDev ? [ From db490f2ae559c395e97c9df6008f376e11ac06f0 Mon Sep 17 00:00:00 2001 From: Dmytro Khrysiev Date: Wed, 29 Apr 2026 20:04:17 +0200 Subject: [PATCH 04/23] feat(orocommerce): updated base --- packages/web/src/app/routes/public-routes.tsx | 2 +- packages/web/src/lib/api.ts | 3 ++- packages/web/vite.config.mts | 17 +++++++++++++++-- 3 files changed, 18 insertions(+), 4 deletions(-) diff --git a/packages/web/src/app/routes/public-routes.tsx b/packages/web/src/app/routes/public-routes.tsx index 6d1b69b32116..02f18e626622 100644 --- a/packages/web/src/app/routes/public-routes.tsx +++ b/packages/web/src/app/routes/public-routes.tsx @@ -10,9 +10,9 @@ import NotFoundPage from './404-page'; import AuthenticatePage from './authenticate'; import { EmbedPage } from './embed'; import { EmbeddedConnectionDialog } from './embed/embedded-connection-dialog'; +import { EmbedCePage } from './embed-ce'; import { McpAuthorizePage } from './mcp-authorize'; import { RedirectPage } from './redirect'; -import { EmbedCePage } from './embed-ce'; const ChatPage = React.lazy(() => import('./chat').then((m) => ({ default: m.ChatPage })), diff --git a/packages/web/src/lib/api.ts b/packages/web/src/lib/api.ts index a7d443834821..c559f54436c3 100644 --- a/packages/web/src/lib/api.ts +++ b/packages/web/src/lib/api.ts @@ -46,7 +46,8 @@ function globalErrorHandler(error: AxiosError) { ) { authenticationSession.logOut(); console.log(errorCode); - window.location.href = '/sign-in'; + // CUSTOMIZATION: use BASE_URL so the redirect lands on the correct subpath + window.location.href = `${import.meta.env.BASE_URL}sign-in`; } } } diff --git a/packages/web/vite.config.mts b/packages/web/vite.config.mts index adba7fa66557..35cf72da4cc4 100644 --- a/packages/web/vite.config.mts +++ b/packages/web/vite.config.mts @@ -49,6 +49,11 @@ export default defineConfig(({ command, mode }) => { Host: '127.0.0.1:4200', }, ws: true, + // CUSTOMIZATION: strip the frontend base prefix before forwarding to + // the backend (which only knows /api/..., not //api/...). + // Works for base='/' (identity) and base='/prefix/' (strips prefix). + rewrite: (path: string) => '/' + path.slice(base.length), + // << CUSTOMIZATION END }, '^/mcp$': { target: 'http://127.0.0.1:3000', @@ -133,11 +138,19 @@ export default defineConfig(({ command, mode }) => { ...(isDev ? [ checker({ + // CUSTOMIZATION START: embedding >> + // Use tsconfig.app.json directly (buildMode: false) to avoid + // tsconfig.spec.json (module: commonjs, no @/* paths) being + // checked during dev serve. The upstream test suite + // dynamically imports source files that use import.meta.env + // and @/ aliases, both of which are incompatible with the + // spec tsconfig's commonjs module setting. typescript: { - buildMode: true, - tsconfigPath: './tsconfig.json', + buildMode: false, + tsconfigPath: './tsconfig.app.json', root: __dirname, }, + // << CUSTOMIZATION END: embedding }), ] : []), From d43716712814216a6ece7cbc7c254c64489a8fe3 Mon Sep 17 00:00:00 2001 From: Dmytro Khrysiev Date: Thu, 30 Apr 2026 17:30:11 +0200 Subject: [PATCH 05/23] feat(orocommerce): updated api --- .../orocommerce/src/lib/common/auth.ts | 20 ++--- .../orocommerce/src/lib/common/client.ts | 75 ++++++++++------ .../orocommerce/src/lib/common/props.ts | 90 +++++++++---------- .../orocommerce/src/lib/common/types.ts | 6 ++ 4 files changed, 109 insertions(+), 82 deletions(-) diff --git a/packages/pieces/community/orocommerce/src/lib/common/auth.ts b/packages/pieces/community/orocommerce/src/lib/common/auth.ts index 9fee7f64c09b..1b705f422f1e 100644 --- a/packages/pieces/community/orocommerce/src/lib/common/auth.ts +++ b/packages/pieces/community/orocommerce/src/lib/common/auth.ts @@ -1,7 +1,7 @@ import { PieceAuth, Property } from '@activepieces/pieces-framework'; -import { HttpMethod } from '@activepieces/pieces-common'; +import { HttpMethod, HttpResponse, HttpMessageBody } from '@activepieces/pieces-common'; import { oroApiCall } from './client'; -import { AppConnectionType } from '@activepieces/shared'; +import { AppConnectionType, tryCatch } from '@activepieces/shared'; export const oroAuth = PieceAuth.CustomAuth({ description: ` @@ -44,25 +44,25 @@ Authenticate to OroCommerce APIs using OAuth 2.0 Client Credentials. }), }, - validate: async ({ auth }) => { - try { - await oroApiCall({ + validate: async ({ auth }): Promise<{ valid: true } | { valid: false; error: string }> => { + const { error } = await tryCatch>(() => + oroApiCall({ method: HttpMethod.GET, resourceUri: 'regions/US-CA', auth: { type: AppConnectionType.CUSTOM_AUTH, props: auth, }, - }); - return { valid: true }; - } catch (e: any) { + }), + ); + if (error) { return { valid: false, - error: - e?.message || + error: error.message || 'Invalid credentials. Please verify your Server URL, Admin Prefix, Client ID, and Client Secret.', }; } + return { valid: true }; }, required: true, diff --git a/packages/pieces/community/orocommerce/src/lib/common/client.ts b/packages/pieces/community/orocommerce/src/lib/common/client.ts index b5f93268c335..5192f3c885b6 100644 --- a/packages/pieces/community/orocommerce/src/lib/common/client.ts +++ b/packages/pieces/community/orocommerce/src/lib/common/client.ts @@ -3,6 +3,7 @@ import { HttpMethod, HttpMessageBody, HttpResponse, + HttpError, AuthenticationType, } from '@activepieces/pieces-common'; @@ -10,16 +11,38 @@ import { type OroAuth, type OroAuthResponseType, type OroApiCallParams, - OroJsonApiItem, - OroJsonApiCollection, + type OroJsonApiItem, + type OroJsonApiCollection, + type FetchCollectionParams, } from './types'; -let cachedToken: string | null = null; -let tokenExpiresAt = 0; +const tokenCache = new Map(); -async function getAccessToken(auth: OroAuth): Promise { - if (cachedToken && Date.now() < tokenExpiresAt) { - return cachedToken; +function buildCacheKey({ auth }: { auth: OroAuth }): string { + return `${auth.props.serverUrl}::${auth.props.clientId}`; +} + +function formatError({ error }: { error: unknown }): string { + if (error instanceof HttpError) { + const status = error.response.status; + const body = error.response.body; + const detail = typeof body === 'object' && body !== null + ? JSON.stringify(body) + : String(body ?? ''); + return `OroCommerce API Error (${status}): ${detail}`; + } + if (error instanceof Error) { + return `OroCommerce API Error: ${error.message}`; + } + return `OroCommerce API Error: ${String(error)}`; +} + +async function getAccessToken({ auth }: { auth: OroAuth }): Promise { + const cacheKey = buildCacheKey({ auth }); + const cached = tokenCache.get(cacheKey); + + if (cached && Date.now() < cached.expiresAt) { + return cached.token; } const baseUrl = auth.props.serverUrl.replace(/\/*$/, ''); @@ -29,17 +52,20 @@ async function getAccessToken(auth: OroAuth): Promise { headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, - body: { + body: new URLSearchParams({ grant_type: 'client_credentials', client_id: auth.props.clientId, client_secret: auth.props.clientSecret, - }, + }).toString(), }); - cachedToken = response.body.access_token; - tokenExpiresAt = Date.now() + response.body.expires_in * 1000 - 30 * 1000; + const token = response.body.access_token; + tokenCache.set(cacheKey, { + token, + expiresAt: Date.now() + response.body.expires_in * 1000 - 30_000, + }); - return cachedToken; + return token; } export async function oroApiCall({ @@ -64,27 +90,21 @@ export async function oroApiCall({ }, authentication: { type: AuthenticationType.BEARER_TOKEN, - token: await getAccessToken(auth), + token: await getAccessToken({ auth }), }, queryParams, body, }); - } catch (error: any) { - const statusCode = error.response?.status; - const errorMessage = - error.response?.data?.message || error.message || 'Unknown error'; - - throw new Error( - `OroCommerce API Error (${statusCode || 'Unknown'}): ${errorMessage}` - ); + } catch (error: unknown) { + throw new Error(formatError({ error })); } } -export async function fetchCollection( - auth: OroAuth, - resourceUri: string, - queryParams?: Record -): Promise { +export async function fetchCollection({ + auth, + resourceUri, + queryParams, +}: FetchCollectionParams): Promise { const response = await oroApiCall({ method: HttpMethod.GET, resourceUri, @@ -92,5 +112,6 @@ export async function fetchCollection( queryParams: { 'page[size]': '50', ...queryParams }, }); - return (response.body as OroJsonApiCollection).data ?? []; + const body = response.body as OroJsonApiCollection | undefined; + return body?.data ?? []; } diff --git a/packages/pieces/community/orocommerce/src/lib/common/props.ts b/packages/pieces/community/orocommerce/src/lib/common/props.ts index a2f6a85572d6..12d1da893c36 100644 --- a/packages/pieces/community/orocommerce/src/lib/common/props.ts +++ b/packages/pieces/community/orocommerce/src/lib/common/props.ts @@ -43,11 +43,11 @@ function buildCustomerOptions( params['filter[searchQuery]'] = `name ~ "${searchValue.trim().replace('"', '')}"`; } - const items = await fetchCollection( - auth as OroAuth, - '/customers', - params - ); + const items = await fetchCollection({ + auth: auth as OroAuth, + resourceUri: '/customers', + queryParams: params, + }); return { options: items.map((item) => ({ label: String(item.attributes['name'] ?? item.id), @@ -101,11 +101,11 @@ export const customerUserDropdown = (required = false) => } params['filter[searchQuery]'] = searchFilters.join(' and '); - const items = await fetchCollection( - auth as OroAuth, - '/customerusers', - params - ); + const items = await fetchCollection({ + auth: auth as OroAuth, + resourceUri: '/customerusers', + queryParams: params, + }); return { options: items.map((item) => { const firstName = String(item.attributes['firstName'] ?? ''); @@ -142,11 +142,11 @@ export const organizationDropdown = Property.Dropdown({ params['filter[searchQuery]'] = `name ~ "${searchValue.trim().replace('"', '')}"`; } - const items = await fetchCollection( - auth as OroAuth, - '/organizations', - params - ); + const items = await fetchCollection({ + auth: auth as OroAuth, + resourceUri: '/organizations', + queryParams: params, + }); return { options: items.map((item) => ({ label: String(item.attributes['name'] ?? item.id), @@ -177,7 +177,7 @@ export const userDropdown = Property.Dropdown({ if (searchValue && searchValue.trim().length > 0) { params['filter[searchQuery]'] = `allText ~ "${searchValue.trim().replace('"', '')}"`; } - const items = await fetchCollection(auth as OroAuth, '/users', params); + const items = await fetchCollection({ auth: auth as OroAuth, resourceUri: '/users', queryParams: params }); return { options: items.map((item) => { const firstName = String(item.attributes['firstName'] ?? ''); @@ -214,7 +214,7 @@ export const websiteDropdown = Property.Dropdown({ if (searchValue && searchValue.trim().length > 0) { params['filter[searchQuery]'] = `name ~ "${searchValue.trim().replace('"', '')}"`; } - const items = await fetchCollection(auth as OroAuth, '/websites', params); + const items = await fetchCollection({ auth: auth as OroAuth, resourceUri: '/websites', queryParams: params }); return { options: items.map((item) => ({ label: String(item.attributes['name'] ?? item.id), @@ -239,10 +239,10 @@ export const invoiceInternalStatusDropdown = Property.Dropdown({ options: async ({ auth }) => { if (!auth) return NOT_CONNECTED; try { - const items = await fetchCollection( - auth as OroAuth, - '/invoiceinternalstatuses' - ); + const items = await fetchCollection({ + auth: auth as OroAuth, + resourceUri: '/invoiceinternalstatuses', + }); return { options: items.map((item) => ({ label: String(item.attributes['name'] ?? item.id), @@ -267,10 +267,10 @@ export const orderInternalStatusDropdown = Property.Dropdown({ options: async ({ auth }) => { if (!auth) return NOT_CONNECTED; try { - const items = await fetchCollection( - auth as OroAuth, - '/orderinternalstatuses' - ); + const items = await fetchCollection({ + auth: auth as OroAuth, + resourceUri: '/orderinternalstatuses', + }); return { options: items.map((item) => ({ label: String(item.attributes['name'] ?? item.id), @@ -368,11 +368,11 @@ export const paymentTermDropdown = Property.Dropdown({ params['filter[searchQuery]'] = `label ~ "${searchValue.trim().replace('"', '')}"`; } - const items = await fetchCollection( - auth as OroAuth, - '/paymentterms', - params - ); + const items = await fetchCollection({ + auth: auth as OroAuth, + resourceUri: '/paymentterms', + queryParams: params, + }); return { options: items.map((item) => ({ label: String(item.attributes['label'] ?? item.id), @@ -404,11 +404,11 @@ export const warehouseDropdown = Property.Dropdown({ if (searchValue && searchValue.trim().length > 0) { params['filter[searchQuery]'] = `name ~ "${searchValue.trim().replace('"', '')}"`; } - const items = await fetchCollection( - auth as OroAuth, - '/warehouses', - params - ); + const items = await fetchCollection({ + auth: auth as OroAuth, + resourceUri: '/warehouses', + queryParams: params, + }); return { options: items.map((item) => ({ label: String(item.attributes['name'] ?? item.id), @@ -441,11 +441,11 @@ export const buildCountryDropdown = (required = false, displayName = 'Country') 'page[size]': '300', }; - const items = await fetchCollection( - auth as OroAuth, - '/countries', - params - ); + const items = await fetchCollection({ + auth: auth as OroAuth, + resourceUri: '/countries', + queryParams: params, + }); return { options: items .filter(function (item) { @@ -501,7 +501,7 @@ export const buildRegionDropdown = ( try { const params: Record = {'filter[country]': countryId}; - const items = await fetchCollection(auth, '/regions', params); + const items = await fetchCollection({ auth, resourceUri: '/regions', queryParams: params }); return { options: items .filter(function (item) { @@ -537,7 +537,7 @@ export const orderStatusDropdown = Property.Dropdown({ options: async ({ auth }) => { if (!auth) return NOT_CONNECTED; try { - const items = await fetchCollection(auth as OroAuth, '/orderstatuses'); + const items = await fetchCollection({ auth: auth as OroAuth, resourceUri: '/orderstatuses' }); return { options: items.map((item) => ({ label: String(item.attributes['name'] ?? item.id), @@ -568,7 +568,7 @@ export const orderDropdown = Property.Dropdown({ if (searchValue && searchValue.trim().length > 0) { params['filter[searchQuery]'] = `allText ~ "${searchValue.trim().replace('"', '')}"`; } - const items = await fetchCollection(auth as OroAuth, '/orders', params); + const items = await fetchCollection({ auth: auth as OroAuth, resourceUri: '/orders', queryParams: params }); return { options: items.map((item) => ({ label: String( @@ -597,9 +597,9 @@ export const productUnitDropdown = Property.Dropdown({ options: async ({ auth }) => { if (!auth) return NOT_CONNECTED; try { - const items = await fetchCollection(auth as OroAuth, '/productunits', { + const items = await fetchCollection({ auth: auth as OroAuth, resourceUri: '/productunits', queryParams: { 'page[size]': '100', - }); + } }); return { options: items.map((item) => ({ // productunits use the string code as id (e.g. "each"), attributes may have label diff --git a/packages/pieces/community/orocommerce/src/lib/common/types.ts b/packages/pieces/community/orocommerce/src/lib/common/types.ts index 36d6fba7e052..d2a96c03eb1a 100644 --- a/packages/pieces/community/orocommerce/src/lib/common/types.ts +++ b/packages/pieces/community/orocommerce/src/lib/common/types.ts @@ -25,6 +25,12 @@ export type OroApiCallParams = { headers?: Record; }; +export type FetchCollectionParams = { + auth: OroAuth; + resourceUri: string; + queryParams?: Record; +}; + export interface OroJsonApiItem { id: string; type: string; From 661e95a5467b46083ae0b55a7624f74d72368c91 Mon Sep 17 00:00:00 2001 From: Dmytro Khrysiev Date: Thu, 30 Apr 2026 17:38:04 +0200 Subject: [PATCH 06/23] feat(orocommerce): updated base --- packages/web/src/app/routes/embed-ce/index.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/web/src/app/routes/embed-ce/index.tsx b/packages/web/src/app/routes/embed-ce/index.tsx index d118e89688e5..c690724daeee 100644 --- a/packages/web/src/app/routes/embed-ce/index.tsx +++ b/packages/web/src/app/routes/embed-ce/index.tsx @@ -150,6 +150,7 @@ const EmbedCePage = React.memo(() => { hideDuplicateFlow: event.data.data.hideDuplicateFlow ?? false, hideFlowsPageNavbar: event.data.data.hideFlowsPageNavbar ?? false, hidePageHeader: event.data.data.hidePageHeader ?? false, + hideTables: event.data.data.hideTables ?? false, }); }); From 124abf08569486cf04efe701d93a0e4c64eeda6e Mon Sep 17 00:00:00 2001 From: Dmytro Khrysiev Date: Tue, 19 May 2026 16:38:03 +0200 Subject: [PATCH 07/23] feat(orocommerce): customized docker --- Dockerfile.oro | 165 +++++++++++++++++++++++++++++++++++++++++ docker-compose.oro.yml | 75 +++++++++++++++++++ 2 files changed, 240 insertions(+) create mode 100644 Dockerfile.oro create mode 100644 docker-compose.oro.yml diff --git a/Dockerfile.oro b/Dockerfile.oro new file mode 100644 index 000000000000..7f1767b1690d --- /dev/null +++ b/Dockerfile.oro @@ -0,0 +1,165 @@ +ARG ORG_BASE_IMAGE=oraclelinux +ARG ORG_IMAGE_TAG=9-slim + + +FROM ${ORG_BASE_IMAGE}:${ORG_IMAGE_TAG} AS base + +ARG ORG_BASE_IMAGE +ARG ORG_IMAGE_TAG + +ENV LANG=en_US.UTF-8 \ + LANGUAGE=en_US:en \ + LC_ALL=en_US.UTF-8 + +# Upgrade base, enable EPEL, install all system deps from official repos +RUN --mount=type=cache,target=/var/cache/dnf \ + < new M().name);\ + process.stdout.write(JSON.stringify(names));\ + " > packages/server/api/dist/src/migration-manifest.json + rm -rf packages/pieces/core packages/pieces/custom + find packages/pieces/community -mindepth 1 -maxdepth 1 -type d \ + ! -name slack \ + ! -name square \ + ! -name facebook-leads \ + ! -name intercom \ + ! -name orocommerce \ + -exec rm -rf {} + + bun install --verbose +EOR + +### STAGE 2: Run ### +FROM base AS run +ARG ORG_BASE_IMAGE +ARG ORG_IMAGE_TAG +WORKDIR /usr/src/app + +ENV AP_PORT=4200 \ + PM2_HOME=/tmp/.pm2 + +LABEL org.opencontainers.image.title="Activepieces" \ + org.opencontainers.image.description="Open-source AI-first workflow automation platform" \ + org.opencontainers.image.authors="Activepieces Inc." \ + org.opencontainers.image.vendor="Activepieces Inc." \ + org.opencontainers.image.url="https://github.com/activepieces/activepieces" \ + org.opencontainers.image.documentation="https://www.activepieces.com/docs" \ + org.opencontainers.image.base.name="${ORG_BASE_IMAGE}:${ORG_IMAGE_TAG}" \ + service="activepieces" + +COPY --link --from=builder /usr/src/app/packages/server/api/src/assets/default.cf /usr/local/etc/isolate +COPY --link docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh +RUN < Date: Wed, 20 May 2026 13:09:03 +0200 Subject: [PATCH 08/23] feat(orocommerce): subfolder install and Docker --- .agents/skills/prefixed-path-rebase/SKILL.md | 268 ++++++++++++++++++ .dockerignore | 3 +- docker-compose.oro.yml | 4 +- packages/server/api/src/app/server.ts | 40 ++- packages/web/src/app/guards/index.tsx | 3 +- .../web/src/app/routes/embed-ce/index.tsx | 6 +- .../components/providers/socket-provider.tsx | 3 +- .../components/third-party-logins.tsx | 5 +- packages/web/src/i18n.ts | 4 +- packages/web/src/lib/api.ts | 5 +- .../web/src/lib/authentication-session.ts | 5 +- packages/web/src/lib/base-path.ts | 7 + packages/web/src/lib/navigation-utils.tsx | 3 +- packages/web/vite.config.mts | 7 +- 14 files changed, 332 insertions(+), 31 deletions(-) create mode 100644 .agents/skills/prefixed-path-rebase/SKILL.md create mode 100644 packages/web/src/lib/base-path.ts diff --git a/.agents/skills/prefixed-path-rebase/SKILL.md b/.agents/skills/prefixed-path-rebase/SKILL.md new file mode 100644 index 000000000000..a7e09fd40e4d --- /dev/null +++ b/.agents/skills/prefixed-path-rebase/SKILL.md @@ -0,0 +1,268 @@ +# Prefixed Path (Subfolder Embedding) — Rebase Instructions + +When upgrading Activepieces to a new version, apply these changes to enable +runtime-configurable subfolder embedding (e.g. `/admin/activepieces-instance/`). + +## Overview + +The stock Activepieces frontend bakes asset paths at build time via Vite's `base` +config. Our customization makes the prefix runtime-configurable via the +`AP_ASSETS_PREFIX` env var, so the same Docker image can serve from any path. + +**How it works:** + +1. Vite builds with `base: './'` → relative asset paths (`assets/index-xxx.js`) +2. Server reads `AP_ASSETS_PREFIX` at startup, rewrites `` in cached `index.html` +3. Frontend reads `` from the DOM at runtime via `basePath` module +4. Nginx strips the prefix before proxying to the container + +## Files to Modify (7 files + 1 new file) + +### 1. NEW: `packages/web/src/lib/base-path.ts` + +Create this file. It reads `` from the DOM at runtime: + +```ts +function resolveBasePath(): string { + const href = document.querySelector('base')?.getAttribute('href'); + if (!href) return '/'; + return href.endsWith('/') ? href : `${href}/`; +} + +export const basePath: string = resolveBasePath(); +``` + +### 2. `packages/web/vite.config.mts` + +Three changes, all marked with `// CUSTOMIZATION START: embedding >>`: + +a. **Add dotenv import** (top of file): + +```ts +import donenv from 'dotenv'; +``` + +b. **Replace `base` default and add dev-only prefix logic** (inside `defineConfig`): + +```ts +donenv.config({ path: path.resolve(__dirname, '../../.env.dev') }); +let base: string = './'; // relative paths so applies at runtime +const allowedHosts: string[] = []; +if (isDev && process.env.AP_FRONTEND_URL) { + const AP_FRONTEND_URL = new URL(process.env.AP_FRONTEND_URL); + const AP_ASSETS_PREFIX = AP_FRONTEND_URL.pathname.replace(/^\/|\/$/, ''); + allowedHosts.push(AP_FRONTEND_URL.host); + base = `/${AP_ASSETS_PREFIX}/`; +} +``` + +Key: production builds use `'./'` (relative). Dev server uses absolute prefix. + +c. **Update server config**: `allowedHosts`, proxy key (`${base}api`), and rewrite: + +```ts +server: { + allowedHosts: allowedHosts, + proxy +: + { + [`${base}api`] + : + { + // ...existing config... + rewrite: (path: string) => '/' + path.slice(base.length), + } + , + } +, +} +, +``` + +d. **Pass `base` to `customHtmlPlugin`**: + +```ts +customHtmlPlugin({ title: AP_TITLE, icon: AP_FAVICON, base }), +``` + +e. **Checker plugin**: use `buildMode: false` with explicit tsconfig path for dev: + +```ts +checker({ + typescript: { + buildMode: false, + tsconfigPath: './tsconfig.app.json', + root: __dirname, + }, +}), +``` + +### 3. `packages/web/src/lib/api.ts` + +Replace `import.meta.env.BASE_URL` with `basePath`: + +```ts +import { basePath } from '@/lib/base-path'; +// ... +export const API_URL = `${API_BASE_URL}${basePath}api`; +// ... +window.location.href = `${basePath}sign-in`; +``` + +### 4. `packages/web/src/lib/authentication-session.ts` + +Replace `import.meta.env.BASE_URL`: + +```ts +import { basePath } from '@/lib/base-path'; +// ... +window.location.href = basePath; // platform switch +window.location.href = `${basePath}sign-in`; // logout +``` + +### 5. `packages/web/src/lib/navigation-utils.tsx` + +Replace `import.meta.env.BASE_URL`: + +```ts +import { basePath } from '@/lib/base-path'; +// ... +const url = `${basePath}${route.replace(/^\//, '')}${...}`; +``` + +### 6. `packages/web/src/components/providers/socket-provider.tsx` + +Replace `import.meta.env.BASE_URL`: + +```ts +import { basePath } from '@/lib/base-path'; +// ... +path: `${basePath}api/socket.io`, +``` + +### 7. `packages/web/src/i18n.ts` + +Replace `import.meta.env.BASE_URL`: + +```ts +import { basePath } from '@/lib/base-path'; +// ... +loadPath: `${basePath}locales/{{lng}}/{{ns}}.json`, +``` + +### 8. `packages/web/src/features/authentication/components/third-party-logins.tsx` + +Replace `import.meta.env.BASE_URL`: + +```ts +import { basePath } from '@/lib/base-path'; +// ... +window.location.href = `${basePath}api/v1/authn/saml/login`; +``` + +### 9. `packages/web/src/app/guards/index.tsx` + +Replace `import.meta.env.BASE_URL` in React Router `basename`: + +```ts +import { basePath } from '@/lib/base-path'; +// ... +basename: basePath, +``` + +### 10. `packages/server/api/src/app/server.ts` + +In the production static-file serving block (`environment !== ApEnvironment.DEVELOPMENT`): + +a. **Add `index: false` and `redirect: false`** to `fastifyStatic` registration. + +b. **Add `allowedPath`** to reject `/` and `/index.html` (forces them to fall through to notFoundHandler): + +```ts +allowedPath: (_pathName, _root, request) => { + const url = (request as { url?: string }).url ?? '' + const cleanUrl = url.split('?')[0] + return cleanUrl !== '/' && cleanUrl !== '/index.html' +}, +``` + +c. **Read and patch index.html at startup** (after `fastifyStatic` registration): + +```ts +const rawIndexHtml = fs.readFileSync(path.join(frontendPath, 'index.html'), 'utf-8') +const assetsPrefix = process.env.AP_ASSETS_PREFIX +const runtimeBaseHref = assetsPrefix ? `/${assetsPrefix.replace(/^\/|\/$/g, '')}/` : '/' +const indexHtml = rawIndexHtml.replace( + //, + ``, +) +``` + +d. **Serve patched HTML in `setNotFoundHandler`**: + +```ts +app.setNotFoundHandler(async (request, reply) => { + if (request.url.startsWith('/api/')) { + return reply.code(404).send({ statusCode: 404, error: 'Not Found', message: 'Route not found' }) + } + if (hasStaticFileExtension(request.url)) { + return reply.code(404).send({ statusCode: 404, error: 'Not Found', message: 'Asset not found' }) + } + return reply.header('Cache-Control', 'no-cache').type('text/html').send(indexHtml) +}) +``` + +## Upgrade Checklist + +When rebasing onto a new Activepieces version: + +1. **Search for new `import.meta.env.BASE_URL` usages**: + ```bash + grep -rn 'import\.meta\.env\.BASE_URL' packages/web/src/ + ``` + Replace each with `basePath` from `@/lib/base-path`. + +2. **Check `server.ts` for changes** to the `fastifyStatic` registration or + `setNotFoundHandler`. Re-apply `index: false`, `redirect: false`, + `allowedPath`, and the `index.html` patching logic. + +3. **Check `vite.config.mts`** for changes to `base`, `server.proxy`, or + `customHtmlPlugin`. Re-apply the `'./'` default and dev-only prefix block. + +4. **Verify `.env.dev`** has `AP_ENVIRONMENT="dev"` (not `"prod"`) for local + development — otherwise the API server tries to serve built frontend files + that don't exist in dev mode. + +## Runtime Configuration + +| Env Var | Where | Example | +|--------------------|------------------------------|--------------------------------------------------| +| `AP_ASSETS_PREFIX` | App container | `admin/activepieces-instance` | +| `AP_FRONTEND_URL` | Worker container, `.env.dev` | `https://myhost.com/admin/activepieces-instance` | + +## Nginx Config + +```nginx +location /admin/activepieces-instance/api/ { + proxy_pass http://activepieces-app:3000/api/; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "Upgrade"; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_buffering off; + proxy_cache off; +} + +location /admin/activepieces-instance/ { + proxy_pass http://activepieces-app:3000/; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "Upgrade"; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-Proto $scheme; +} +``` + +Trailing `/` on `proxy_pass` strips the prefix. The server receives root-relative paths. + diff --git a/.dockerignore b/.dockerignore index c8c810369ed1..0d4ceea40a84 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,6 +1,7 @@ .angular .dockerignore **/.env +**/.env.* .git .gitattributes .github @@ -14,4 +15,4 @@ deploy Dockerfile dist docs -**/node_modules \ No newline at end of file +**/node_modules diff --git a/docker-compose.oro.yml b/docker-compose.oro.yml index fe80cecdc0c7..5992e260ce91 100644 --- a/docker-compose.oro.yml +++ b/docker-compose.oro.yml @@ -24,6 +24,7 @@ services: - AP_PIECES_SOURCE=CLOUD_AND_DB - AP_PIECES_SYNC_MODE=OFFICIAL_AUTO - AP_DEV_PIECES=orocommerce + - AP_ENVIRONMENT=prod volumes: - ./cache:/usr/src/app/cache - ./dev/config:/usr/src/app/config @@ -47,11 +48,12 @@ services: - AP_REDIS_TYPE=STANDALONE - AP_REDIS_HOST=redis - AP_REDIS_PORT=6379 - - AP_FRONTEND_URL=http://activepieces-app:8080 + - AP_FRONTEND_URL=http://activepieces-app:4200 - AP_LOG_PRETTY=false - AP_PIECES_SOURCE=CLOUD_AND_DB - AP_PIECES_SYNC_MODE=OFFICIAL_AUTO - AP_DEV_PIECES=orocommerce + - AP_ENVIRONMENT=prod deploy: replicas: 2 volumes: diff --git a/packages/server/api/src/app/server.ts b/packages/server/api/src/app/server.ts index 537c3c55f9aa..0935603883fb 100644 --- a/packages/server/api/src/app/server.ts +++ b/packages/server/api/src/app/server.ts @@ -1,3 +1,4 @@ +import fs from 'fs' import path from 'path' import { ApEnvironment, apId, ApMultipartFile, spreadIfDefined } from '@activepieces/shared' import cors from '@fastify/cors' @@ -64,6 +65,13 @@ export const setupServer = async (): Promise => { const frontendPath = path.resolve(process.cwd(), 'dist/packages/web') await app.register(fastifyStatic, { root: frontendPath, + index: false, + redirect: false, + allowedPath: (_pathName: string, _root: string, request: { url?: string }) => { + const url = request.url ?? '' + const cleanUrl = url.split('?')[0] + return cleanUrl !== '/' && cleanUrl !== '/index.html' + }, setHeaders: (res, filepath) => { const normalized = filepath.replace(/\\/g, '/') if (normalized.endsWith('.html')) { @@ -77,20 +85,30 @@ export const setupServer = async (): Promise => { } }, }) - } - app.setNotFoundHandler(async (request, reply) => { - if (request.url.startsWith('/api/')) { - return reply.code(404).send({ statusCode: 404, error: 'Not Found', message: 'Route not found' }) - } - if (system.isApp() && environment !== ApEnvironment.DEVELOPMENT) { + const rawIndexHtml = fs.readFileSync(path.join(frontendPath, 'index.html'), 'utf-8') + const assetsPrefix = process.env.AP_ASSETS_PREFIX + const runtimeBaseHref = assetsPrefix ? `/${assetsPrefix.replace(/^\/|\/$/g, '')}/` : '/' + const indexHtml = rawIndexHtml.replace( + //, + ``, + ) + + app.setNotFoundHandler(async (request, reply) => { + if (request.url.startsWith('/api/')) { + return reply.code(404).send({ statusCode: 404, error: 'Not Found', message: 'Route not found' }) + } if (hasStaticFileExtension(request.url)) { return reply.code(404).send({ statusCode: 404, error: 'Not Found', message: 'Asset not found' }) } - return reply.sendFile('index.html') - } - return reply.code(404).send({ statusCode: 404, error: 'Not Found', message: 'Route not found' }) - }) + return reply.header('Cache-Control', 'no-cache').type('text/html').send(indexHtml) + }) + } + else { + app.setNotFoundHandler(async (_request, reply) => { + return reply.code(404).send({ statusCode: 404, error: 'Not Found', message: 'Route not found' }) + }) + } app.addHook('onSend', async (_request, reply) => { void reply.header('X-Content-Type-Options', 'nosniff') @@ -212,5 +230,3 @@ function convertDatesToStrings(data: unknown): unknown { } return data } - - diff --git a/packages/web/src/app/guards/index.tsx b/packages/web/src/app/guards/index.tsx index 8ff6408fdcf0..7dd5d3fc74af 100644 --- a/packages/web/src/app/guards/index.tsx +++ b/packages/web/src/app/guards/index.tsx @@ -12,6 +12,7 @@ import { projectRoutes } from '@/app/routes/project-routes'; import { publicRoutes } from '@/app/routes/public-routes'; import { RouteLoadingBar } from '@/components/custom/route-loading-bar'; import { useEmbedding } from '@/components/providers/embed-provider'; +import { basePath } from '@/lib/base-path'; import { AllowOnlyLoggedInUserOnlyGuard } from '../components/allow-logged-in-user-only-guard'; import { ProjectDashboardLayout } from '../components/project-layout'; @@ -70,7 +71,7 @@ const routes = [ export const memoryRouter = createMemoryRouter(routes); const browserRouter = createBrowserRouter(routes, { - basename: import.meta.env.BASE_URL, + basename: basePath, }); const ApRouter = () => { diff --git a/packages/web/src/app/routes/embed-ce/index.tsx b/packages/web/src/app/routes/embed-ce/index.tsx index c690724daeee..c4ea35eb82dd 100644 --- a/packages/web/src/app/routes/embed-ce/index.tsx +++ b/packages/web/src/app/routes/embed-ce/index.tsx @@ -134,6 +134,7 @@ const EmbedCePage = React.memo(() => { disableNavigationInBuilder: event.data.data.disableNavigationInBuilder !== false, hideFolders: event.data.data.hideFolders ?? false, + hideTables: event.data.data.hideTables ?? false, sdkVersion: event.data.data.sdkVersion, fontUrl: event.data.data.fontUrl, fontFamily: event.data.data.fontFamily, @@ -150,12 +151,13 @@ const EmbedCePage = React.memo(() => { hideDuplicateFlow: event.data.data.hideDuplicateFlow ?? false, hideFlowsPageNavbar: event.data.data.hideFlowsPageNavbar ?? false, hidePageHeader: event.data.data.hidePageHeader ?? false, - hideTables: event.data.data.hideTables ?? false, }); }); memoryRouter.navigate(initialRoute); - handleVendorNavigation({ projectId }); + if (projectId) { + handleVendorNavigation({ projectId: projectId }); + } handleClientNavigation(); notifyVendorPostAuthentication(); }; diff --git a/packages/web/src/components/providers/socket-provider.tsx b/packages/web/src/components/providers/socket-provider.tsx index 36df3c9deab6..de2e7b76fcb1 100644 --- a/packages/web/src/components/providers/socket-provider.tsx +++ b/packages/web/src/components/providers/socket-provider.tsx @@ -4,10 +4,11 @@ import { toast } from 'sonner'; import { API_BASE_URL } from '@/lib/api'; import { authenticationSession } from '@/lib/authentication-session'; +import { basePath } from '@/lib/base-path'; const socket = io(API_BASE_URL, { transports: ['websocket'], - path: `${import.meta.env.BASE_URL}api/socket.io`, + path: `${basePath}api/socket.io`, autoConnect: false, reconnection: true, }); diff --git a/packages/web/src/features/authentication/components/third-party-logins.tsx b/packages/web/src/features/authentication/components/third-party-logins.tsx index 0d5264e29156..2f34fb01e9ef 100644 --- a/packages/web/src/features/authentication/components/third-party-logins.tsx +++ b/packages/web/src/features/authentication/components/third-party-logins.tsx @@ -13,6 +13,7 @@ import { Button } from '@/components/ui/button'; import { internalErrorToast } from '@/components/ui/sonner'; import { oauth2Utils } from '@/features/connections/utils/oauth2-utils'; import { flagsHooks } from '@/hooks/flags-hooks'; +import { basePath } from '@/lib/base-path'; const ThirdPartyIcon = ({ icon }: { icon: string }) => { return icon; @@ -46,9 +47,7 @@ const ThirdPartyLogin = React.memo(({ isSignUp }: { isSignUp: boolean }) => { }; const signInWithSaml = () => - (window.location.href = `${ - import.meta.env.BASE_URL - }api/v1/authn/saml/login`); + (window.location.href = `${basePath}api/v1/authn/saml/login`); return (
diff --git a/packages/web/src/i18n.ts b/packages/web/src/i18n.ts index 4da78bd5162b..12278787d862 100644 --- a/packages/web/src/i18n.ts +++ b/packages/web/src/i18n.ts @@ -5,6 +5,8 @@ import Backend from 'i18next-http-backend'; import ICU from 'i18next-icu'; import { initReactI18next } from 'react-i18next'; +import { basePath } from '@/lib/base-path'; + i18n .use(ICU) .use(Backend) @@ -21,7 +23,7 @@ i18n nsSeparator: false, returnEmptyString: false, backend: { - loadPath: `${import.meta.env.BASE_URL}locales/{{lng}}/{{ns}}.json`, + loadPath: `${basePath}locales/{{lng}}/{{ns}}.json`, }, }); export default i18n; diff --git a/packages/web/src/lib/api.ts b/packages/web/src/lib/api.ts index c559f54436c3..0b8af96db96f 100644 --- a/packages/web/src/lib/api.ts +++ b/packages/web/src/lib/api.ts @@ -9,12 +9,13 @@ import axios, { import qs from 'qs'; import { authenticationSession } from '@/lib/authentication-session'; +import { basePath } from '@/lib/base-path'; export const isRunningCloudInDevMode = import.meta.env.MODE === 'cloud'; export const API_BASE_URL = isRunningCloudInDevMode ? 'https://cloud.activepieces.com' : window.location.origin; -export const API_URL = `${API_BASE_URL}${import.meta.env.BASE_URL}api`; +export const API_URL = `${API_BASE_URL}${basePath}api`; const disallowedRoutes = [ '/v1/managed-authn/external-token', @@ -47,7 +48,7 @@ function globalErrorHandler(error: AxiosError) { authenticationSession.logOut(); console.log(errorCode); // CUSTOMIZATION: use BASE_URL so the redirect lands on the correct subpath - window.location.href = `${import.meta.env.BASE_URL}sign-in`; + window.location.href = `${basePath}sign-in`; } } } diff --git a/packages/web/src/lib/authentication-session.ts b/packages/web/src/lib/authentication-session.ts index 018d343675ad..9eb1344631fb 100644 --- a/packages/web/src/lib/authentication-session.ts +++ b/packages/web/src/lib/authentication-session.ts @@ -8,6 +8,7 @@ import dayjs from 'dayjs'; import { jwtDecode } from 'jwt-decode'; import { authenticationApi } from '@/api/authentication-api'; +import { basePath } from '@/lib/base-path'; import { ApStorage } from './ap-browser-storage'; const tokenKey = 'token'; @@ -105,7 +106,7 @@ export const authenticationSession = { if (!isNil(result.projectId)) { ApStorage.getInstance().setItem(projectIdKey, result.projectId); } - window.location.href = import.meta.env.BASE_URL; + window.location.href = basePath; }, switchToProject(projectId: string) { if (authenticationSession.getProjectId() === projectId) { @@ -127,7 +128,7 @@ export const authenticationSession = { }, logOut() { this.clearSession(); - window.location.href = `${import.meta.env.BASE_URL}sign-in`; + window.location.href = `${basePath}sign-in`; }, }; diff --git a/packages/web/src/lib/base-path.ts b/packages/web/src/lib/base-path.ts new file mode 100644 index 000000000000..871dc410af1b --- /dev/null +++ b/packages/web/src/lib/base-path.ts @@ -0,0 +1,7 @@ +function resolveBasePath(): string { + const href = document.querySelector('base')?.getAttribute('href'); + if (!href) return '/'; + return href.endsWith('/') ? href : `${href}/`; +} + +export const basePath: string = resolveBasePath(); diff --git a/packages/web/src/lib/navigation-utils.tsx b/packages/web/src/lib/navigation-utils.tsx index 23fef18bf768..147a7dfade7e 100644 --- a/packages/web/src/lib/navigation-utils.tsx +++ b/packages/web/src/lib/navigation-utils.tsx @@ -1,6 +1,7 @@ import { useNavigate, useSearchParams } from 'react-router-dom'; import { useEmbedding } from '@/components/providers/embed-provider'; +import { basePath } from '@/lib/base-path'; export const useNewWindow = () => { const { embedState } = useEmbedding(); @@ -13,7 +14,7 @@ export const useNewWindow = () => { }); } else { return (route: string, searchParams?: string) => { - const url = `${import.meta.env.BASE_URL}${route.replace(/^\//, '')}${ + const url = `${basePath}${route.replace(/^\//, '')}${ searchParams ? '?' + searchParams : '' }`; window.open(url, '_blank', 'noopener noreferrer'); diff --git a/packages/web/vite.config.mts b/packages/web/vite.config.mts index ed6365ef59d3..2f2a9c11f98d 100644 --- a/packages/web/vite.config.mts +++ b/packages/web/vite.config.mts @@ -18,11 +18,10 @@ export default defineConfig(({ command, mode }) => { const AP_FAVICON = 'https://activepieces.com/favicon.ico'; // CUSTOMIZATION START: embedding >> - // TODO: we will need to support real .env files loading? donenv.config({ path: path.resolve(__dirname, '../../.env.dev') }); - let base = '/'; - const allowedHosts = []; - if (process.env.AP_FRONTEND_URL) { + let base: string = './'; + const allowedHosts: string[] = []; + if (isDev && process.env.AP_FRONTEND_URL) { const AP_FRONTEND_URL = new URL(process.env.AP_FRONTEND_URL); const AP_ASSETS_PREFIX = AP_FRONTEND_URL.pathname.replace(/^\/|\/$/, ''); allowedHosts.push(AP_FRONTEND_URL.host); From e53030d50912fc897effda68d891c087386cab60 Mon Sep 17 00:00:00 2001 From: Dmytro Khrysiev Date: Wed, 20 May 2026 15:13:33 +0200 Subject: [PATCH 09/23] feat(orocommerce): subfolder install and Docker --- Dockerfile.oro | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/Dockerfile.oro b/Dockerfile.oro index 7f1767b1690d..c314a21a9338 100644 --- a/Dockerfile.oro +++ b/Dockerfile.oro @@ -60,13 +60,14 @@ RUN < Date: Thu, 21 May 2026 15:00:57 +0200 Subject: [PATCH 10/23] feat(orocommerce): subfolder install and Docker --- .agents/skills/prefixed-path-rebase/SKILL.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.agents/skills/prefixed-path-rebase/SKILL.md b/.agents/skills/prefixed-path-rebase/SKILL.md index a7e09fd40e4d..c72e2310e796 100644 --- a/.agents/skills/prefixed-path-rebase/SKILL.md +++ b/.agents/skills/prefixed-path-rebase/SKILL.md @@ -244,7 +244,7 @@ When rebasing onto a new Activepieces version: ```nginx location /admin/activepieces-instance/api/ { - proxy_pass http://activepieces-app:3000/api/; + proxy_pass http://activepieces-app:4200?request_uri; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "Upgrade"; @@ -255,7 +255,7 @@ location /admin/activepieces-instance/api/ { } location /admin/activepieces-instance/ { - proxy_pass http://activepieces-app:3000/; + proxy_pass http://activepieces-app:4200?request_uri; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "Upgrade"; From 0cfb944ee9fbfae39e5d4a8e1421f118639302dc Mon Sep 17 00:00:00 2001 From: Dmytro Khrysiev Date: Mon, 25 May 2026 13:07:49 +0200 Subject: [PATCH 11/23] feat(orocommerce): update doc --- .agents/skills/prefixed-path-rebase/SKILL.md | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/.agents/skills/prefixed-path-rebase/SKILL.md b/.agents/skills/prefixed-path-rebase/SKILL.md index c72e2310e796..bcdcdad120ff 100644 --- a/.agents/skills/prefixed-path-rebase/SKILL.md +++ b/.agents/skills/prefixed-path-rebase/SKILL.md @@ -240,7 +240,7 @@ When rebasing onto a new Activepieces version: | `AP_ASSETS_PREFIX` | App container | `admin/activepieces-instance` | | `AP_FRONTEND_URL` | Worker container, `.env.dev` | `https://myhost.com/admin/activepieces-instance` | -## Nginx Config +## Nginx Config for local development ```nginx location /admin/activepieces-instance/api/ { @@ -264,5 +264,18 @@ location /admin/activepieces-instance/ { } ``` +## Nginx Config for docker + +```nginx +location /admin/activepieces-instance/ { + proxy_pass http://127.0.0.1:4200/; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "Upgrade"; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-Proto $scheme; +} +``` + Trailing `/` on `proxy_pass` strips the prefix. The server receives root-relative paths. From f43f88092873848788aa413f19a5d66fb8510634 Mon Sep 17 00:00:00 2001 From: Viacheslav Dubrovskyi Date: Sun, 7 Jun 2026 13:58:45 +0200 Subject: [PATCH 12/23] OPI-1597: Create CI build for activepieces feat: update configuration and Docker setup for improved deployment - Added environment variables to .env.example for better configuration management. - Enhanced Dockerfile with multi-stage builds for optimized image size and build efficiency. - Introduced Jenkinsfile for CI/CD pipeline integration. - Updated docker-compose.oro.yml to streamline service definitions and health checks. - Added healthcheck script for better container health monitoring. - Updated bun.lock to include new dependencies for the orocommerce piece. - Created docker-bake.hcl for build configuration management. --- .env.example | 13 +++- Dockerfile.oro | 171 +++++++++++++++++++---------------------- Jenkinsfile | 32 ++++++++ bun.lock | 12 +++ docker-bake.hcl | 31 ++++++++ docker-compose.oro.yml | 112 +++++++++++++-------------- healthcheck | 15 ++++ 7 files changed, 235 insertions(+), 151 deletions(-) create mode 100644 Jenkinsfile create mode 100644 docker-bake.hcl create mode 100755 healthcheck diff --git a/.env.example b/.env.example index ceb1deea9cd5..89c4fa2929d7 100644 --- a/.env.example +++ b/.env.example @@ -11,18 +11,29 @@ AP_ENCRYPTION_KEY= ## JWT Secret AP_JWT_SECRET= +AP_PORT=4200 AP_ENVIRONMENT=prod AP_FRONTEND_URL=http://localhost:8080 AP_WEBHOOK_TIMEOUT_SECONDS=30 AP_TRIGGER_DEFAULT_POLL_INTERVAL=5 + AP_POSTGRES_DATABASE=activepieces AP_POSTGRES_HOST=postgres AP_POSTGRES_PORT=5432 AP_POSTGRES_USERNAME=postgres -AP_POSTGRES_PASSWORD= +AP_POSTGRES_PASSWORD=change_me + AP_EXECUTION_MODE=UNSANDBOXED +AP_QUEUE_MODE=REDIS +AP_REDIS_TYPE=STANDALONE AP_REDIS_HOST=redis AP_REDIS_PORT=6379 + AP_FLOW_TIMEOUT_SECONDS=600 AP_TELEMETRY_ENABLED=true AP_TEMPLATES_SOURCE_URL="https://cloud.activepieces.com/api/v1/flow-templates" + +AP_LOG_PRETTY=false +AP_PIECES_SOURCE=CLOUD_AND_DB +AP_PIECES_SYNC_MODE=OFFICIAL_AUTO +AP_DEV_PIECES=orocommerce diff --git a/Dockerfile.oro b/Dockerfile.oro index c314a21a9338..abc5ee53ce4c 100644 --- a/Dockerfile.oro +++ b/Dockerfile.oro @@ -1,110 +1,91 @@ ARG ORG_BASE_IMAGE=oraclelinux ARG ORG_IMAGE_TAG=9-slim +ARG BUN_VERSION=1.3.3 - +FROM oven/bun:${BUN_VERSION} AS bun +### STAGE 1: Base ### FROM ${ORG_BASE_IMAGE}:${ORG_IMAGE_TAG} AS base -ARG ORG_BASE_IMAGE -ARG ORG_IMAGE_TAG +ARG NODE_VERSION=24 ENV LANG=en_US.UTF-8 \ LANGUAGE=en_US:en \ LC_ALL=en_US.UTF-8 -# Upgrade base, enable EPEL, install all system deps from official repos -RUN --mount=type=cache,target=/var/cache/dnf \ - < Date: Sun, 7 Jun 2026 14:44:03 +0200 Subject: [PATCH 13/23] feat(ci): add latest tag creation for Docker images on main branch success --- Jenkinsfile | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/Jenkinsfile b/Jenkinsfile index 6068ad7ac00a..2ebec1137cb8 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -13,6 +13,7 @@ pipeline { environment { ORO_PROJECT = 'ocir.eu-frankfurt-1.oci.oraclecloud.com/frecfpcrj6gd/oro-product-development/' ORO_AP_IMAGE = "${ORO_PROJECT}activepieces" + ORO_AP_IMAGE_TAG=${env.BUILD_TAG.replaceAll('/', '-').replaceAll('%2F', '-')} } stages { @@ -27,6 +28,27 @@ pipeline { docker buildx bake -f docker-bake.hcl --progress=plain --push ''' } - } + } + post { + success { + script { + def branchName = env.BRANCH_NAME ?: env.GIT_BRANCH ?: '' + def isMainBranch = branchName == 'main' || branchName == 'origin/main' || branchName == 'refs/heads/main' + + if (isMainBranch) { + sh label: 'docker image set latest tag', script: ''' + docker buildx imagetools create \ + -t ${ORO_AP_IMAGE}:latest \ + ${ORO_AP_IMAGE}:${ORO_AP_IMAGE_TAG} + ''' + } + } + } + always { + sh label: 'docker logout ocir.eu-frankfurt-1.oci.oraclecloud.com', script: ''' + docker logout ocir.eu-frankfurt-1.oci.oraclecloud.com || true + ''' + } + } } } From 4e0c678bce4f10734f8c2411e40c8d0f4593d865 Mon Sep 17 00:00:00 2001 From: Viacheslav Dubrovskyi Date: Sun, 7 Jun 2026 14:55:38 +0200 Subject: [PATCH 14/23] feat(ci): update image tagging logic for Docker builds --- Jenkinsfile | 29 +++++++++-------------------- docker-bake.hcl | 2 +- 2 files changed, 10 insertions(+), 21 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index 2ebec1137cb8..88f747b4df7e 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -13,12 +13,14 @@ pipeline { environment { ORO_PROJECT = 'ocir.eu-frankfurt-1.oci.oraclecloud.com/frecfpcrj6gd/oro-product-development/' ORO_AP_IMAGE = "${ORO_PROJECT}activepieces" - ORO_AP_IMAGE_TAG=${env.BUILD_TAG.replaceAll('/', '-').replaceAll('%2F', '-')} } stages { stage('Build') { steps { + script { + env.ORO_AP_IMAGE_TAG = env.BUILD_TAG.replaceAll('/', '-').replaceAll('%2F', '-') + } withCredentials([usernamePassword(credentialsId: 'ocir.eu-frankfurt-1.oci.oraclecloud.com', usernameVariable: 'ORO_REGISTRY_CREDS_USR', passwordVariable: 'ORO_REGISTRY_CREDS_PSW')]) { sh label: 'docker login ocir.eu-frankfurt-1.oci.oraclecloud.com', script: 'echo $ORO_REGISTRY_CREDS_PSW | docker login -u $ORO_REGISTRY_CREDS_USR --password-stdin ocir.eu-frankfurt-1.oci.oraclecloud.com' } @@ -29,26 +31,13 @@ pipeline { ''' } } - post { - success { - script { - def branchName = env.BRANCH_NAME ?: env.GIT_BRANCH ?: '' - def isMainBranch = branchName == 'main' || branchName == 'origin/main' || branchName == 'refs/heads/main' + } - if (isMainBranch) { - sh label: 'docker image set latest tag', script: ''' - docker buildx imagetools create \ - -t ${ORO_AP_IMAGE}:latest \ - ${ORO_AP_IMAGE}:${ORO_AP_IMAGE_TAG} - ''' - } - } - } - always { - sh label: 'docker logout ocir.eu-frankfurt-1.oci.oraclecloud.com', script: ''' - docker logout ocir.eu-frankfurt-1.oci.oraclecloud.com || true - ''' - } + post { + always { + sh label: 'docker logout ocir.eu-frankfurt-1.oci.oraclecloud.com', script: ''' + docker logout ocir.eu-frankfurt-1.oci.oraclecloud.com || true + ''' } } } diff --git a/docker-bake.hcl b/docker-bake.hcl index ed6bca915384..3f0279d32cd8 100644 --- a/docker-bake.hcl +++ b/docker-bake.hcl @@ -25,7 +25,7 @@ group "default" { target "runtime" { target = "runtime" dockerfile = "Dockerfile.oro" - tags = ["${ORO_AP_IMAGE}:${ORO_AP_IMAGE_TAG}"] + tags = concat(["${ORO_AP_IMAGE}:${ORO_AP_IMAGE_TAG}"], GIT_BRANCH == "main" ? ["${ORO_AP_IMAGE}:latest"] : []) labels = labelList() // platforms = ["linux/amd64", "linux/arm64"] } From 64081d7ed7bafcc8a915a712162223b87b804d6e Mon Sep 17 00:00:00 2001 From: Viacheslav Dubrovskyi Date: Sun, 7 Jun 2026 15:02:16 +0200 Subject: [PATCH 15/23] feat(ci): enhance image tag formatting in Jenkinsfile --- Jenkinsfile | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Jenkinsfile b/Jenkinsfile index 88f747b4df7e..4037e5cd27ed 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -19,7 +19,12 @@ pipeline { stage('Build') { steps { script { - env.ORO_AP_IMAGE_TAG = env.BUILD_TAG.replaceAll('/', '-').replaceAll('%2F', '-') + env.ORO_AP_IMAGE_TAG = env.BUILD_TAG + .toLowerCase() + .replaceAll('%2F', '-') + .replaceAll('[^a-z0-9._-]', '-') + .replaceAll('-+', '-') + .take(128) } withCredentials([usernamePassword(credentialsId: 'ocir.eu-frankfurt-1.oci.oraclecloud.com', usernameVariable: 'ORO_REGISTRY_CREDS_USR', passwordVariable: 'ORO_REGISTRY_CREDS_PSW')]) { sh label: 'docker login ocir.eu-frankfurt-1.oci.oraclecloud.com', script: 'echo $ORO_REGISTRY_CREDS_PSW | docker login -u $ORO_REGISTRY_CREDS_USR --password-stdin ocir.eu-frankfurt-1.oci.oraclecloud.com' From ff22d46a896199eaf553e0c2f046b38724459896 Mon Sep 17 00:00:00 2001 From: Viacheslav Dubrovskyi Date: Sun, 7 Jun 2026 15:06:26 +0200 Subject: [PATCH 16/23] feat(ci): ensure concurrent builds are disabled in Jenkins pipeline --- Jenkinsfile | 1 + 1 file changed, 1 insertion(+) diff --git a/Jenkinsfile b/Jenkinsfile index 4037e5cd27ed..bef9ebf5a6fd 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -8,6 +8,7 @@ pipeline { timeout(time: 30, unit: 'MINUTES') ansiColor('xterm') timestamps() + disableConcurrentBuilds(abortPrevious: true) } environment { From 52ba732faa90cbe45ae45cf3b12cf88f0c22f382 Mon Sep 17 00:00:00 2001 From: Dmytro Khrysiev Date: Tue, 9 Jun 2026 19:10:16 +0200 Subject: [PATCH 17/23] feat(orocommerce): added orocommerce piece to requirements --- bun.lock | 13 +++++++++++++ package.json | 1 + 2 files changed, 14 insertions(+) diff --git a/bun.lock b/bun.lock index 55993ec22b00..3814e30409dd 100644 --- a/bun.lock +++ b/bun.lock @@ -6,6 +6,7 @@ "name": "activepieces", "dependencies": { "@activepieces/import-fresh-webpack": "3.3.0", + "@activepieces/piece-orocommerce": "0.2.0", "@ai-sdk/amazon-bedrock": "3.0.97", "@ai-sdk/anthropic": "^3.0.0", "@ai-sdk/azure": "^3.0.0", @@ -5271,6 +5272,16 @@ "tslib": "^2.3.0", }, }, + "packages/pieces/community/orocommerce": { + "name": "@activepieces/piece-orocommerce", + "version": "0.2.0", + "dependencies": { + "@activepieces/pieces-common": "workspace:*", + "@activepieces/pieces-framework": "workspace:*", + "@activepieces/shared": "workspace:*", + "tslib": "2.6.2", + }, + }, "packages/pieces/community/outseta": { "name": "@activepieces/piece-outseta", "version": "0.1.1", @@ -9669,6 +9680,8 @@ "@activepieces/piece-orimon": ["@activepieces/piece-orimon@workspace:packages/pieces/community/orimon"], + "@activepieces/piece-orocommerce": ["@activepieces/piece-orocommerce@workspace:packages/pieces/community/orocommerce"], + "@activepieces/piece-outseta": ["@activepieces/piece-outseta@workspace:packages/pieces/community/outseta"], "@activepieces/piece-paddle": ["@activepieces/piece-paddle@workspace:packages/pieces/community/paddle"], diff --git a/package.json b/package.json index 59f8df9f9a3a..ddef56023550 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ }, "private": true, "dependencies": { + "@activepieces/piece-orocommerce": "0.2.0", "@activepieces/import-fresh-webpack": "3.3.0", "@ai-sdk/amazon-bedrock": "3.0.97", "@ai-sdk/anthropic": "^3.0.0", From 5555ffe7341e6b89d97115901eaf38f692e7cfba Mon Sep 17 00:00:00 2001 From: Viacheslav Dubrovskyi Date: Fri, 12 Jun 2026 10:39:17 +0200 Subject: [PATCH 18/23] feat(env): update .env.example and add .env.oro.example for configuration feat(docker): modify Dockerfile.oro to allow bun to update lockfile fix(bun.lock): remove orocommerce package references fix(docker-compose): change postgres image to official postgres:18 --- .env.example | 13 +------------ .env.oro.example | 39 +++++++++++++++++++++++++++++++++++++++ Dockerfile.oro | 4 +++- bun.lock | 12 ------------ docker-compose.oro.yml | 2 +- 5 files changed, 44 insertions(+), 26 deletions(-) create mode 100644 .env.oro.example diff --git a/.env.example b/.env.example index 89c4fa2929d7..ceb1deea9cd5 100644 --- a/.env.example +++ b/.env.example @@ -11,29 +11,18 @@ AP_ENCRYPTION_KEY= ## JWT Secret AP_JWT_SECRET= -AP_PORT=4200 AP_ENVIRONMENT=prod AP_FRONTEND_URL=http://localhost:8080 AP_WEBHOOK_TIMEOUT_SECONDS=30 AP_TRIGGER_DEFAULT_POLL_INTERVAL=5 - AP_POSTGRES_DATABASE=activepieces AP_POSTGRES_HOST=postgres AP_POSTGRES_PORT=5432 AP_POSTGRES_USERNAME=postgres -AP_POSTGRES_PASSWORD=change_me - +AP_POSTGRES_PASSWORD= AP_EXECUTION_MODE=UNSANDBOXED -AP_QUEUE_MODE=REDIS -AP_REDIS_TYPE=STANDALONE AP_REDIS_HOST=redis AP_REDIS_PORT=6379 - AP_FLOW_TIMEOUT_SECONDS=600 AP_TELEMETRY_ENABLED=true AP_TEMPLATES_SOURCE_URL="https://cloud.activepieces.com/api/v1/flow-templates" - -AP_LOG_PRETTY=false -AP_PIECES_SOURCE=CLOUD_AND_DB -AP_PIECES_SYNC_MODE=OFFICIAL_AUTO -AP_DEV_PIECES=orocommerce diff --git a/.env.oro.example b/.env.oro.example new file mode 100644 index 000000000000..89c4fa2929d7 --- /dev/null +++ b/.env.oro.example @@ -0,0 +1,39 @@ +## It's advisable to consult the documentation and use the tools/deploy.sh to generate the passwords, keys, instead of manually filling them. + +AP_ENGINE_EXECUTABLE_PATH=dist/packages/engine/main.js + +## Random Long Password (Optional for community edition) +AP_API_KEY= + +## 256 bit encryption key, 32 hex character +AP_ENCRYPTION_KEY= + +## JWT Secret +AP_JWT_SECRET= + +AP_PORT=4200 +AP_ENVIRONMENT=prod +AP_FRONTEND_URL=http://localhost:8080 +AP_WEBHOOK_TIMEOUT_SECONDS=30 +AP_TRIGGER_DEFAULT_POLL_INTERVAL=5 + +AP_POSTGRES_DATABASE=activepieces +AP_POSTGRES_HOST=postgres +AP_POSTGRES_PORT=5432 +AP_POSTGRES_USERNAME=postgres +AP_POSTGRES_PASSWORD=change_me + +AP_EXECUTION_MODE=UNSANDBOXED +AP_QUEUE_MODE=REDIS +AP_REDIS_TYPE=STANDALONE +AP_REDIS_HOST=redis +AP_REDIS_PORT=6379 + +AP_FLOW_TIMEOUT_SECONDS=600 +AP_TELEMETRY_ENABLED=true +AP_TEMPLATES_SOURCE_URL="https://cloud.activepieces.com/api/v1/flow-templates" + +AP_LOG_PRETTY=false +AP_PIECES_SOURCE=CLOUD_AND_DB +AP_PIECES_SYNC_MODE=OFFICIAL_AUTO +AP_DEV_PIECES=orocommerce diff --git a/Dockerfile.oro b/Dockerfile.oro index abc5ee53ce4c..d83b814a94f1 100644 --- a/Dockerfile.oro +++ b/Dockerfile.oro @@ -77,7 +77,9 @@ COPY --link packages/ ./packages/ RUN --mount=type=cache,target=/root/.bun/install/cache < Date: Thu, 18 Jun 2026 16:19:36 +0200 Subject: [PATCH 19/23] feat(orocommerce): update .env.oro.example --- .env.oro.example | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.env.oro.example b/.env.oro.example index 89c4fa2929d7..c7fd2b3bf472 100644 --- a/.env.oro.example +++ b/.env.oro.example @@ -13,7 +13,8 @@ AP_JWT_SECRET= AP_PORT=4200 AP_ENVIRONMENT=prod -AP_FRONTEND_URL=http://localhost:8080 +AP_FRONTEND_URL=http://localhost:8080/admin/activepieces-instance +AP_ASSETS_PREFIX=admin/activepieces-instance AP_WEBHOOK_TIMEOUT_SECONDS=30 AP_TRIGGER_DEFAULT_POLL_INTERVAL=5 From 7b7995cb5a339a3004f34bf162e59040867ac035 Mon Sep 17 00:00:00 2001 From: Dmytro Khrysiev Date: Mon, 22 Jun 2026 18:23:27 +0200 Subject: [PATCH 20/23] feat(orocommerce): added orocommerce SKILL for actions creation --- .../orocommerce-action-builder/SKILL.md | 304 ++++++++++++++++++ 1 file changed, 304 insertions(+) create mode 100644 .agents/skills/orocommerce-action-builder/SKILL.md diff --git a/.agents/skills/orocommerce-action-builder/SKILL.md b/.agents/skills/orocommerce-action-builder/SKILL.md new file mode 100644 index 000000000000..00df1bb637d6 --- /dev/null +++ b/.agents/skills/orocommerce-action-builder/SKILL.md @@ -0,0 +1,304 @@ +--- +name: orocommerce-action-builder +description: Creates new OroCommerce piece actions from an OpenAPI specification. Use when the user asks to add a create/update/delete action to the OroCommerce piece, or provides an OpenAPI/Swagger spec for an OroCommerce resource endpoint. +--- + +# OroCommerce Action Builder + +Build OroCommerce actions from an OpenAPI spec with minimum reading. +**Piece root:** `packages/pieces/community/orocommerce/src/` + +## Decision tree + +``` +Spec provided? + YES → Step 1: Parse spec + NO → Ask for the OpenAPI YAML/JSON or the resource name so you can consult the spec +``` + +--- + +## Step 1 — Parse the spec (read only what you need) + +From the spec extract: + +| Item | Where to find it | +|---|---| +| Resource name (JSON:API `type`) | `POST /admin/api/{resource}` → `requestBody` → `data.type` | +| Create attributes | `POST` body schema → `data.attributes` properties | +| Update attributes | `PATCH /admin/api/{resource}/{id}` body → `data.attributes` | +| Relationships (create) | `POST` body → `data.relationships` keys + each `data.type` | +| Relationships (update) | `PATCH` body → `data.relationships` keys + each `data.type` | +| Required fields | `POST` body `required` array or spec description ("Example:" block is usually the minimum viable payload) | + +> **Shortcut:** the spec description for `POST` and `PATCH` always contains a literal JSON example. Read that example — it shows every required field and the exact relationship type strings. Ignore all other spec noise. + +--- + +## Step 2 — Map attributes → `Property` types + +| Attribute characteristic | `Property` type | +|---|---| +| Short string (name, code, email, username, title) | `Property.ShortText` | +| Long string (description, notes, body) | `Property.LongText` | +| Date string (`YYYY-MM-DD`) | `Property.ShortText` with description `"YYYY-MM-DD format"` | +| Boolean flag (enabled, confirmed, locked) | `Property.Checkbox` | +| Numeric string passed as-is | `Property.ShortText` | +| JSON sub-object / freeform map | `Property.Json` | +| Enum with known values | `Property.StaticDropdown` listing the values | + +Required on create → `required: true`. Optional → `required: false`. +On **update** actions every attribute is `required: false` (only provided fields are patched). + +--- + +## Step 3 — Map relationships → dropdowns + `buildRels` + +Each relationship in the spec has a `type` string (e.g. `"businessunits"`, `"userroles"`). Use this table to pick the right dropdown and `buildRels` entry: + +| JSON:API `type` | Dropdown to use | `buildRels` call | +|---|---|---| +| `organizations` (single) | `organizationDropdown` | `organization: ['organizations', p.organization]` | +| `organizations` (multi) | `organizationsDropdown` | `organizations: ['organizations', p.organizations, true]` | +| `businessunits` (single owner) | `businessUnitRequiredDropdown` (create) / `businessUnitDropdown` (update) | hard-coded: `owner: { data: { type: 'businessunits', id: p.owner ?? '' } }` | +| `businessunits` (multi) | `businessUnitDropdown` | `businessUnits: ['businessunits', p.businessUnits, true]` | +| `users` (owner/sales rep) | `userDropdown` | `owner: ['users', p.owner]` | +| `customers` (required) | `customerRequiredDropdown` | hard-coded: `customer: { data: { type: 'customers', id: p.customer ?? '' } }` | +| `customers` (optional) | `customerDropdown` | `customer: ['customers', p.customer]` | +| `websites` | `websiteDropdown` | `website: ['websites', p.website]` | +| `customeruserroles` | `customerUserRoleDropdown` | `userRoles: ['customeruserroles', p.userRoles, true]` | +| `userroles` | `userRoleDropdown` | `userRoles: ['userroles', p.userRoles, true]` | +| `usergroups` | `userGroupDropdown` | `groups: ['usergroups', p.groups, true]` | +| `userauthstatuses` | `userAuthStatusDropdown` | `auth_status: ['userauthstatuses', p.authStatus]` | +| `paymentterms` | `paymentTermDropdown` | `paymentTerm: ['paymentterms', p.paymentTerm]` | +| `warehouses` | `warehouseDropdown` | `warehouse: ['warehouses', p.warehouse]` | +| `products` | `productDropdown` | `product: ['products', p.product]` | +| `customergroups` | `customerGroupDropdown` | `group: ['customergroups', p.group]` | +| `customertaxcodes` | `customerTaxCodeDropdown` | `taxCode: ['customertaxcodes', p.taxCode]` | +| `orderinternalstatuses` | `orderInternalStatusDropdown` | `internalStatus: ['orderinternalstatuses', p.internalStatus]` | +| `invoiceinternalstatuses` | `invoiceInternalStatusDropdown` | `internalStatus: ['invoiceinternalstatuses', p.internalStatus]` | +| Any unknown type | Add a new `makeSearchableDropdown` / `makeEnumDropdown` in `props.ts` | Same pattern | + +**`many=true` rule:** use `true` as the third `buildRels` argument whenever the spec shows `"data": [...]` (array). Omit it (single) when spec shows `"data": {...}`. + +--- + +## Step 4 — Add missing dropdowns (only if needed) + +Check the **Existing dropdowns** table above. If the relationship type is already covered, import it — do not re-create it. + +If a dropdown is missing, add it to `src/lib/common/props.ts` following the established patterns: + +**Searchable (most relationships):** +```ts +export const myThingDropdown = makeSearchableDropdown({ + displayName: 'My Thing', + description: 'Search my things by name.', + resourceUri: '/mythings', // JSON:API collection path + fieldsParam: 'id,name', + searchExpr: (q) => `name ~ "${q}"`, + labelFn: attrLabel('name'), +}); +``` + +**Enum (status/code lists — small static sets):** +```ts +export const myStatusDropdown = makeEnumDropdown({ + displayName: 'Status', + description: 'Select a status.', + resourceUri: '/mystatuses', + labelFn: attrLabel('name', 'id'), // fallback to id when name absent +}); +``` + +Export the new dropdown from `src/lib/common/index.ts` via the existing `export * from './props'` — no extra line needed. + +--- + +## Step 5 — Write the action files + +### Create action template (`src/lib/actions/create-{resource}.ts`) + +```ts +import { createAction, Property } from '@activepieces/pieces-framework'; +import { HttpMethod } from '@activepieces/pieces-common'; +import { + oroAuth, oroApiCall, + // ...dropdowns for this action... + additionalAttributesProp, additionalRelationsProp, additionalHeadersProp, +} from '../common'; +import { OroAuth } from '../common/types'; +import { jsonApiBodyUtils } from '../common/jsonapi-body-utils'; + +export const create{Resource}Action = createAction({ + auth: oroAuth, + name: 'create_{resource}', // snake_case, permanent + displayName: 'Create {Resource}', + description: 'Creates a new {resource} record in OroCommerce.', + props: { + // --- Required attributes --- + fieldName: Property.ShortText({ displayName: 'Field Name', required: true }), + + // --- Optional attributes --- + optField: Property.ShortText({ displayName: 'Optional Field', required: false }), + + // --- Required relationships --- + owner: businessUnitRequiredDropdown, // if owner is required + + // --- Optional relationships --- + organization: organizationDropdown, + + additionalAttributes: additionalAttributesProp, + additionalRelations: additionalRelationsProp, + additionalHeaders: additionalHeadersProp, + }, + + async run(context) { + const p = context.propsValue; + const extraAttrs = jsonApiBodyUtils.parseAdditionalAttributes(p.additionalAttributes); + const extraRels = jsonApiBodyUtils.parseAdditionalRelations(p.additionalRelations); + + const attributes = { + fieldName: p.fieldName, // required → always included + ...jsonApiBodyUtils.pickDefined({ // optional → included only when non-null + optField: p.optField, + }), + ...extraAttrs, + }; + + const relationships = { + owner: { data: { type: 'businessunits', id: p.owner ?? '' } }, // required rel + ...jsonApiBodyUtils.buildRels({ + organization: ['organizations', p.organization], + }), + ...extraRels, + }; + + const response = await oroApiCall({ + method: HttpMethod.POST, + resourceUri: '/{resources}', + auth: context.auth as OroAuth, + body: { data: { type: '{resources}', attributes, relationships } }, + headers: p.additionalHeaders as Record, + }); + + return response.body; + }, +}); +``` + +### Update action template (`src/lib/actions/update-{resource}.ts`) + +Key differences from create: +- First prop is `{resource}Id: Property.ShortText({ required: true })` (the record to patch) +- Every attribute is `required: false`; wrap ALL in `jsonApiBodyUtils.pickDefined` +- Every relationship is optional — put all in `buildRels`, no hard-coded required rel +- HTTP method is `HttpMethod.PATCH`, URI is `/{resources}/${p.{resource}Id}` +- Body `data` includes `id: p.{resource}Id` alongside `type` + +```ts +const response = await oroApiCall({ + method: HttpMethod.PATCH, + resourceUri: `/{resources}/${p.{resource}Id}`, + auth: context.auth as OroAuth, + body: { + data: { + type: '{resources}', + id: p.{resource}Id, + attributes, + relationships, + }, + }, + headers: p.additionalHeaders as Record, +}); +``` + +--- + +## Step 6 — Wire up + +**Three files to touch (always):** + +### `src/lib/actions/index.ts` +```ts +export { create{Resource}Action } from './create-{resource}'; +export { update{Resource}Action } from './update-{resource}'; +``` + +### `src/index.ts` +Add to the `import` and to the `actions: [...]` array: +```ts +import { create{Resource}Action, update{Resource}Action } from './lib/actions'; + +// inside createPiece actions array: +create{Resource}Action, +update{Resource}Action, +``` + +### Bump `package.json` version +Increment patch version (e.g. `0.3.0` → `0.3.1`) — required so live flows pick up the change. + +--- + +## Step 7 — Verify + +```bash +npx turbo run lint --filter=@activepieces/piece-orocommerce +``` + +Must exit with `0 errors`. Fix any lint issues before finishing. + +--- + +## Quick-look reference + +### File locations + +| File | Purpose | +|---|---| +| `src/lib/actions/create-{resource}.ts` | New create action | +| `src/lib/actions/update-{resource}.ts` | New update action | +| `src/lib/actions/index.ts` | Re-exports all actions | +| `src/lib/common/props.ts` | All shared dropdowns | +| `src/lib/common/client.ts` | `oroApiCall`, `fetchCollection` | +| `src/lib/common/jsonapi-body-utils.ts` | `pickDefined`, `buildRels`, `parseAdditional*` | +| `src/index.ts` | Piece registration | + +### `buildRels` signatures recap + +```ts +// Single relationship (data: { type, id }) +relName: ['json-api-type', p.propValue] + +// Single wrapped in array (data: [{ type, id }]) — many = true +relName: ['json-api-type', p.propValue, true] +``` + +Values that are `null`, `undefined`, or `''` are automatically skipped by `buildRels`. + +### `oroApiCall` signature recap + +```ts +await oroApiCall({ + method: HttpMethod.POST | HttpMethod.PATCH | HttpMethod.GET | HttpMethod.DELETE, + resourceUri: '/collection' | '/collection/${id}', + auth: context.auth as OroAuth, + body?: Record, + queryParams?: Record, + headers?: Record, +}); +// returns { body: unknown, status: number } +``` + +--- + +## Critical reminders + +1. **`name` is permanent** — once published, `name: 'create_xyz'` must never change; flows store it. +2. **Required rels on create** — hard-code them as `{ data: { type, id: p.x ?? '' } }` outside `buildRels`; `buildRels` skips empty strings which would silently omit a required rel. +3. **`additionalAttributes/Relations/Headers` always present** — add all three to every action for extensibility. +4. **`pickDefined` for optional attributes** — prevents sending `null`/`undefined` to the API on updates. +5. **Multi-value rels need `many: true`** — check the spec example: `"data": [...]` → `true`, `"data": {...}` → omit. +6. **Lint must pass** — unused imports are lint errors; import only the dropdowns the action actually uses. +7. **Bump `package.json` version** — patch bump for every change; without it live flows never get your fix. + From 3afd96f749ada07c405b7a0f587dd4a5352d07e2 Mon Sep 17 00:00:00 2001 From: Dmytro Khrysiev Date: Fri, 26 Jun 2026 11:11:46 +0200 Subject: [PATCH 21/23] feat(orocommerce): update .env.oro.example --- .env.oro.example | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.env.oro.example b/.env.oro.example index c7fd2b3bf472..1b0596cc601e 100644 --- a/.env.oro.example +++ b/.env.oro.example @@ -31,7 +31,7 @@ AP_REDIS_HOST=redis AP_REDIS_PORT=6379 AP_FLOW_TIMEOUT_SECONDS=600 -AP_TELEMETRY_ENABLED=true +AP_TELEMETRY_ENABLED=false AP_TEMPLATES_SOURCE_URL="https://cloud.activepieces.com/api/v1/flow-templates" AP_LOG_PRETTY=false From 25904245ce6c1c8d8d5abf9f45c218b1692fc188 Mon Sep 17 00:00:00 2001 From: Dmytro Khrysiev Date: Mon, 20 Jul 2026 18:41:55 +0200 Subject: [PATCH 22/23] feat(orocommerce): embed-ce upgrade to match the latest codebase --- packages/web/src/app/routes/embed-ce/index.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/web/src/app/routes/embed-ce/index.tsx b/packages/web/src/app/routes/embed-ce/index.tsx index 5e3784ac7916..f002fffe1dc9 100644 --- a/packages/web/src/app/routes/embed-ce/index.tsx +++ b/packages/web/src/app/routes/embed-ce/index.tsx @@ -155,6 +155,8 @@ const EmbedCePage = React.memo(() => { hideDuplicateFlow: event.data.data.hideDuplicateFlow ?? false, hideFlowsPageNavbar: event.data.data.hideFlowsPageNavbar ?? false, hidePageHeader: event.data.data.hidePageHeader ?? false, + hideActiveUsers: event.data.data.hideActiveUsers ?? false, + hideGlobalSearch: event.data.data.hideGlobalSearch ?? false, }); }); From fd5cf3a11dcab459711615b20730d551cb5c9892 Mon Sep 17 00:00:00 2001 From: Dmytro Khrysiev Date: Wed, 5 Aug 2026 14:37:43 +0200 Subject: [PATCH 23/23] feat(orocommerce): host mapping and skill --- .agents/skills/piece-outbound-hosts/SKILL.md | 145 ++++ scripts/piece-hostnames.js | 146 ++++ scripts/piece-outbound-hosts.md | 730 +++++++++++++++++++ 3 files changed, 1021 insertions(+) create mode 100644 .agents/skills/piece-outbound-hosts/SKILL.md create mode 100755 scripts/piece-hostnames.js create mode 100644 scripts/piece-outbound-hosts.md diff --git a/.agents/skills/piece-outbound-hosts/SKILL.md b/.agents/skills/piece-outbound-hosts/SKILL.md new file mode 100644 index 000000000000..ea5eeaac5ed8 --- /dev/null +++ b/.agents/skills/piece-outbound-hosts/SKILL.md @@ -0,0 +1,145 @@ +--- +name: piece-outbound-hosts +description: Use when regenerating or updating scripts/piece-outbound-hosts.md — the mapping of which hostnames each Activepieces piece connects to, used to request an outbound-connection allowlist from cloud/infra for a customer's chosen piece list. Triggers on "update the outbound hosts doc", "regenerate the piece hostname mapping", "what hosts does piece X call", or when new pieces have been added since the doc was last generated. +--- + +# Piece Outbound Hostnames Mapping + +Produces/maintains `scripts/piece-outbound-hosts.md`: every piece classified into one of three +buckets by how its hostname is determined, so infra can be asked for the right thing per piece — +a literal host, a wildcard on a known vendor suffix, or "ask the customer for their server URL". + +**Tool:** `node scripts/piece-hostnames.js --all` scans every piece under +`packages/pieces/{community,core}/*/src` for literal `https?://` URLs and splits each into valid +hostnames vs. ones containing an unresolved `${...}`/`{...}` placeholder. It is the raw-data step +for Bucket 1 and part of Bucket 2 below — it does **not** do the classification or the dropdown +resolution; that's manual, per this skill. Run `node scripts/piece-hostnames.js ` for a +single piece, `--file names.txt` for a specific list, `--all` for the full sweep. + +## The three buckets + +A piece's hostname is determined one of three ways. Get the bucket wrong and the allowlist request +is either useless (asking for a literal that's actually customer-specific) or impossible to fulfill +(asking for a wildcard on a domain the customer's server doesn't share). + +| Bucket | How the host is determined | Ask infra for | +|---|---|---| +| 1. Static | Literal string in source, always the same | The literal hostname(s) | +| 2. Dynamic | Vendor-fixed domain + a runtime piece (subdomain, region, account ID) | A wildcard on the fixed suffix, or the customer's specific value substituted in | +| 3. Arbitrary / self-hosted | The customer's connection supplies the **entire** server URL — no vendor domain at all | The customer's exact URL — nothing else can be pre-computed | + +### Bucket 1 — Static + +Default bucket. `piece-hostnames.js --all` output for a piece that has ≥1 valid hostname and no +Bucket 3 auth field. Take the `hosts` list as-is, minus doc-link noise (see Cleanup below). + +### Bucket 2 — Dynamic + +Two ways a piece lands here, and the script only catches one of them: + +- **Visible in source**: the literal URL contains `${var}` or `{var}` inside the hostname — + `piece-hostnames.js` already flags these (they fail its hostname-char validation and get + reported separately). Example: `` `https://${subdomain}.zendesk.com` ``. +- **Invisible to the script**: the host comes from a value returned by the OAuth token exchange + itself (e.g. Zoho's `api_domain` field), or from a connection field read via `auth.props.X` + with **no literal `https://` anywhere in source** — there is nothing for the regex to match. + You only find these by reading the piece's `auth.ts` / `common/*.ts` for prop names like + `location`, `region`, `site`, `pod`, `subdomain`, `account`, `environment`, `cloud`. + +For every Bucket-2 piece, check whether the placeholder is driven by a +`Property.StaticDropdown` in the same auth file: + +- **Dropdown found** → read every `value:` in its `options.options` array and substitute each + into the template — you now have the complete, finite, real hostname list. Put it in Section 2A. + (Real example: `zoho-mail`'s `location` dropdown has 6 values — `zoho.com`, `zoho.eu`, `zoho.in`, + `zoho.com.au`, `zoho.jp`, `zohocloud.ca` — giving 6 real `accounts.` hosts, not one + unresolvable placeholder.) +- **No dropdown, free `Property.ShortText`** → the value is genuinely customer-specific, but the + domain suffix around it is still fixed in code. Put it in Section 2B: state the pattern + (`.fixed-suffix.com`) and what to ask the customer for (usually: whatever their own + product's UI calls that value — "workspace subdomain", "account ID", "site name" — check the + field's `displayName`/`description` for the exact term). + +**Multi-part hosts**: some vendors split OAuth login and API calls onto different hosts that both +depend on the same selector (Microsoft's cloud dropdown drives both `login.microsoftonline.com` +and `graph.microsoft.com`; Zoho's location drives both `accounts.` and a product-specific +`.` or `www.zohoapis.`). Resolve and list both. + +### Bucket 3 — Arbitrary / self-hosted + +Grep the piece's auth definition (`auth.ts`, or inline in `index.ts`) for a prop named +`serverUrl`, `instanceUrl`, `hostUrl`, `siteUrl`, `baseUrl`, `workspaceUrl`, or `domain`, then read +how it's used. The test is **not** the field's example text or description — it's whether the +code appends any fixed suffix at all: + +```ts +// Bucket 3 — value used as-is, nothing appended +url: `${auth.props.serverUrl}/oauth2-token` + +// Bucket 2 — value used as-is too, but check: is there a DIFFERENT fixed-suffix host elsewhere +// in the same piece (e.g. a separate OAuth login endpoint)? If so it's a hybrid — document both. +``` + +A field's description hinting at a conventional domain (`service-now`'s instanceUrl example is +`dev12345.service-now.com`, `okta`'s domain example is `dev-12345.okta.com`) does **not** make it +Bucket 2 — the code accepts literally any string. Only an enforced suffix in the URL-building code +moves it to Bucket 2. When in doubt, find the line that builds the final request URL and check +whether a vendor domain literal appears next to the customer value. + +Also flag self-hostable open-source integrations even when the piece ships a fixed SaaS default +(`posthog`, `umami`, `mattermost`, `mautic`, `chatwoot`, `discourse`, `matomo`, `ghostcms`, +`gitlab`, `gitea`, `nocodb`, `fountain`) — Section 1 lists their default correctly, but note in +Section 3 that a self-hosting customer's real host overrides it. + +## Cleanup — known noise in the raw scan + +Before trusting `piece-hostnames.js --all` output, strip these (they come up every regeneration): + +- **Query-string artifacts**: a template like `` `${baseUrl}${resourceUri}` `` right after a real + static URL literal (e.g. `` `https://api.foo.com${resourceUri}` ``) makes the regex capture + `api.foo.com${resourceuri}` as one invalid "hostname" and drop the real static host entirely. + Fix: if the invalid entry's prefix up to the placeholder is itself a valid host with a real TLD + (`api.foo.com`), it's this artifact — keep the prefix as a normal Bucket-1 host, discard the rest. +- **Markdown-emphasis artifacts**: trailing `**`/`_`/`&size=64` glued onto an otherwise valid host + from bolded/italicized description text (e.g. `demo.crm.dynamics.com**`, `lobstermail.ai**`). + Strip trailing punctuation before judging validity. +- **Shared sample-data lists**: if several unrelated pieces list the exact same ~20-30 domains + (`www.linkedin.com`, `www.crunchbase.com`, `angel.co`, …), that's a copy-pasted trigger + `sampleData` object, not a real call. Drop for all of them, note it once in the doc's caveats. +- **Doc/help links from `description:` text**: hosts like `developers.hubspot.com`, + `support.google.com`, `docs.nocodb.com` are pulled from human-readable help text, not API calls. + They're mostly harmless to list (worst case infra allowlists an unused doc domain) — leave them + in Section 1 rather than hand-auditing 700 pieces, but say so once in the doc's caveats so infra + knows to sanity-check, not silently trust every entry. +- **Placeholder example domains**: filter hosts matching `example.com`, `yoursite.*`, + `yourcompany.*`, `mycompany.*`, `acme.*`, `contoso.*` — these are illustrative text in + descriptions, not real hosts, and would otherwise pollute Section 1. + +## Regenerating incrementally + +Don't redo the whole classification from scratch every time. Diff which piece directories changed +since the doc's last "Generated" date (`git log --since= --name-only -- packages/pieces/`, +or just re-run `--all` and diff its output against the doc's Section 1 table) and only reclassify +the new/changed pieces. Merge their rows into the existing three sections, keep the rest untouched, +and bump the "Generated" footer date + note. + +## Output shape + +`scripts/piece-outbound-hosts.md`: intro + caveats, then Section 1 (table: piece | hosts), Section +2 split into 2A (resolved placeholder → real hostnames, table: piece | placeholder | real +hostnames) and 2B (piece | pattern | what to ask the customer), Section 3 (table: piece | auth +field | typical/example value, with the self-hostable-OSS callout as its own short list). Keep the +caveats paragraph at the top — it's load-bearing, not boilerplate: it's what tells whoever reads +the doc that Section 1 wasn't hand-audited entry-by-entry. + +## Common mistakes + +- Trusting a dropdown's **label** text instead of its `value:` — labels are human-readable + (`'zoho.eu (Europe)'`) but the value is what actually goes in the URL (`'zoho.eu'`). +- Classifying by the auth field's **name** alone. `subdomain`/`domain`-named fields are Bucket 2 + in some pieces (fixed suffix appended) and Bucket 3 in others (used bare, e.g. `okta`) — always + check the URL-building code, not just the prop name. +- Missing OAuth-response-derived hosts (Zoho's `api_domain`) because the script found nothing — + absence of a script hit is not proof the piece has no dynamic host; check the auth file too. +- Re-running full classification on all ~700 pieces for a one-piece doc update. Scope the work to + what changed. diff --git a/scripts/piece-hostnames.js b/scripts/piece-hostnames.js new file mode 100755 index 000000000000..e8caaa038d51 --- /dev/null +++ b/scripts/piece-hostnames.js @@ -0,0 +1,146 @@ +#!/usr/bin/env node +// Usage: node scripts/piece-hostnames.js [piece-name...] +// node scripts/piece-hostnames.js --file names.txt +// node scripts/piece-hostnames.js --all (every piece under both PIECES_ROOTS) +// +// Scans each piece's source for literal https?:// URLs and prints the +// distinct hostnames found, so an outbound-connection allowlist can be +// requested from cloud/infra for a given set of pieces. +// +// Heuristic, not exhaustive: it only catches hostnames written as string +// literals in the piece's own code. It cannot see URLs built dynamically +// (e.g. `${subdomain}.example.com`) or hosts only reachable via a shared +// OAuth/base-URL passed in from elsewhere. Review the output before +// sending it to infra. + +const fs = require('fs'); +const path = require('path'); + +const PIECES_ROOTS = [ + path.join(__dirname, '..', 'packages', 'pieces', 'community'), + path.join(__dirname, '..', 'packages', 'pieces', 'core'), +]; + +const IGNORED_HOSTS = new Set([ + 'activepieces.com', + 'www.activepieces.com', + 'cdn.activepieces.com', + 'example.com', + 'localhost', + 'schema.org', + 'json-schema.org', +]); + +const URL_RE = /https?:\/\/[^\s'"`)>,;]+/g; +const VALID_HOSTNAME_RE = /^[a-z0-9.-]+$/i; + +function slugify(name) { + return name.toLowerCase().trim().replace(/[\s_]+/g, '-').replace(/[^a-z0-9-]/g, ''); +} + +function findPieceDir(name) { + const slug = slugify(name); + for (const root of PIECES_ROOTS) { + const direct = path.join(root, slug); + if (fs.existsSync(direct)) return direct; + } + for (const root of PIECES_ROOTS) { + const collapsed = slug.replace(/-/g, ''); + const match = fs.readdirSync(root).find((dir) => dir.replace(/-/g, '') === collapsed); + if (match) return path.join(root, match); + } + return null; +} + +function walkTsFiles(dir) { + const results = []; + for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { + if (entry.name === 'node_modules' || entry.name === 'dist') continue; + const full = path.join(dir, entry.name); + if (entry.isDirectory()) { + results.push(...walkTsFiles(full)); + } else if (entry.name.endsWith('.ts') && !entry.name.endsWith('.spec.ts')) { + results.push(full); + } + } + return results; +} + +function extractHostnames(pieceDir) { + const hosts = new Set(); + const dynamicHosts = new Set(); + for (const file of walkTsFiles(path.join(pieceDir, 'src'))) { + const content = fs.readFileSync(file, 'utf8'); + for (const match of content.matchAll(URL_RE)) { + try { + const hostname = new URL(match[0]).hostname; + if (IGNORED_HOSTS.has(hostname)) continue; + if (VALID_HOSTNAME_RE.test(hostname)) { + hosts.add(hostname); + } else { + dynamicHosts.add(hostname); + } + } catch { + // not a parseable URL, skip + } + } + } + return { hosts, dynamicHosts }; +} + +function main() { + const args = process.argv.slice(2); + if (args.length === 0) { + console.error('Usage: node scripts/piece-hostnames.js [piece-name...]'); + console.error(' node scripts/piece-hostnames.js --file names.txt'); + console.error(' node scripts/piece-hostnames.js --all'); + process.exit(1); + } + + const names = + args[0] === '--file' + ? fs + .readFileSync(args[1], 'utf8') + .split('\n') + .map((line) => line.trim()) + .filter(Boolean) + : args[0] === '--all' + ? PIECES_ROOTS.flatMap((root) => fs.readdirSync(root)).sort() + : args; + + const allHosts = new Set(); + const allDynamicHosts = new Set(); + const notFound = []; + + for (const name of names) { + const dir = findPieceDir(name); + if (!dir) { + notFound.push(name); + continue; + } + const { hosts, dynamicHosts } = extractHostnames(dir); + console.log(`${name}:`); + for (const host of [...hosts].sort()) { + console.log(` ${host}`); + allHosts.add(host); + } + for (const host of [...dynamicHosts].sort()) { + console.log(` ${host} (dynamic — placeholder in code, check src for real values)`); + allDynamicHosts.add(host); + } + } + + console.log('\n# Combined unique hostnames'); + for (const host of [...allHosts].sort()) console.log(host); + + if (allDynamicHosts.size) { + console.log('\n# Dynamic hostnames — NOT resolvable from source alone, check manually'); + for (const host of [...allDynamicHosts].sort()) console.log(host); + } + + if (notFound.length) { + console.error(`\n# Could not resolve these piece names: ${notFound.join(', ')}`); + } +} + +main(); diff --git a/scripts/piece-outbound-hosts.md b/scripts/piece-outbound-hosts.md new file mode 100644 index 000000000000..0b73bdf97309 --- /dev/null +++ b/scripts/piece-outbound-hosts.md @@ -0,0 +1,730 @@ +# Piece Outbound Hostnames + +Reference for requesting an outbound-connection allowlist from cloud/infra for a given list of customer-selected pieces. Generated by scanning each piece's source for literal `https?://` URLs ([`scripts/piece-hostnames.js`](../scripts/piece-hostnames.js)) plus manual verification of every piece whose host is built at runtime (dynamic subdomain, region selector, or fully customer-supplied server). + +**How to use this doc for a customer's piece list:** +1. Look each piece up in [Section 1](#1-static-hostnames). Most pieces resolve to a small, fixed set of hostnames — allowlist those directly. +2. If the piece isn't in Section 1 (or you want to sanity-check it), check [Section 2](#2-dynamic-hostnames) — the piece calls a hostname that's partly or fully templated (a per-customer subdomain, account ID, or region on a known vendor domain). +3. If it's not in either, check [Section 3](#3-arbitrary--self-hosted-server-pieces) — the piece connects to a server URL the customer supplies wholesale (their own deployment), so there is no vendor domain to allowlist at all. + +**Caveats — this is static analysis, not a runtime trace:** +- A hostname appearing in a piece's source doesn't guarantee the piece calls it at runtime — some entries are documentation/help links pulled from field `description` text (e.g. `developers.hubspot.com`), not API endpoints. Sanity-check an unfamiliar-looking host before allowlisting it. +- Conversely, this **cannot** see a hostname built entirely from a runtime value with no literal text in source (e.g. Zoho's OAuth response tells the app which regional API domain to use) — those are covered by hand in Section 2 instead. +- `chargekeep`, `linka`, `sperse`, and `upgradechat` each list ~28 identical social/business-directory domains (`www.linkedin.com`, `www.crunchbase.com`, `angel.co`, …) — these come from a shared trigger's `sampleData`, not real outbound calls. Ignore them. +- `moxie-crm`'s and a few other pieces' lists carry incidental third-party domains that appear only inside embedded HTML/copy (Google Meet links, gstatic/googleusercontent asset hosts) — real if the piece actually renders that content, noise if it's just quoted in a description. + +--- + +## 1. Static hostnames + +One row per piece with a fixed, literal hostname (or small fixed set) in its source. Comma-separated where a piece calls more than one host. + +| Piece | Hostnames | +|---|---| +| activecampaign | www.github.com | +| activepieces | cloud.activepieces.com | +| acuity-scheduling | acuityscheduling.com | +| acumbamail | acumbamail.com | +| add-event | api.addevent.com, dashboard.addevent.com | +| afforai | api.afforai.com | +| agentx | api.agentx.so, www.agentx.so | +| ai | gateway.ai.cloudflare.com | +| aianswer | app.aianswer.us, backend-development-e8jn.onrender.com | +| aidbase | api.aidbase.ai | +| aiprise | api-sandbox.aiprise.com, api.aiprise.com, app.aiprise.com | +| air-ops | api.airops.com | +| aircall | api.aircall.io | +| airparser | api.airparser.com | +| airtable | airtable.com, api.airtable.com, content.airtable.com | +| airtop | api.airtop.ai, portal.airtop.ai, proxy.example.com, www.google.com | +| alai | getalai.com, slides-api.getalai.com | +| alt-text-ai | alttext.ai | +| alttextify | alttextify.net, api.alttextify.net | +| amazon-s3 | console.aws.amazon.com, github.com | +| amazon-sns | docs.aws.amazon.com | +| ampeco | m.intercharge.eu | +| anyhook-graphql | 10.0.0.101 | +| anyhook-websocket | 10.0.0.101 | +| apify | api.apify.com, docs.apify.com | +| apitable | aitable.ai, apitable.com, help.aitable.ai | +| apitemplate-io | app.apitemplate.io, rest-alt-de.apitemplate.io, rest-alt-us.apitemplate.io, rest-alt.apitemplate.io, rest-au.apitemplate.io, rest-de.apitemplate.io, rest-us.apitemplate.io, rest.apitemplate.io | +| apollo | api.apollo.io, app.apollo.io, docs.apollo.io | +| appfollow | api.appfollow.io, watch.appfollow.io | +| asana | app.asana.com | +| ashby | api.ashbyhq.com | +| ask-handle | dashboard.askhandle.com | +| asknews | api.asknews.app, api.asknews.com, asknews.app | +| assembled | api.assembledhq.com, app.assembledhq.com | +| assemblyai | api.assemblyai.com, www.assemblyai.com | +| attio | api.attio.com, app.attio.com, docs.attio.com | +| autocalls | app.autocalls.ai | +| avian | api.avian.io, avian.io | +| avoma | api.avoma.com, app.avoma.com, help.avoma.com, meet.avoma.com, zoom.us | +| aws-bedrock | console.aws.amazon.com | +| azure-ad | graph.microsoft.com, learn.microsoft.com, login.microsoftonline.com | +| azure-devops | dev.azure.com, learn.microsoft.com | +| bamboohr | api.bamboohr.com, documentation.bamboohr.com | +| bannerbear | api.bannerbear.com, sync.api.bannerbear.com | +| barcode-lookup | api.barcodelookup.com | +| baremetrics | api.baremetrics.com, app.baremetrics.com | +| baserow | api.baserow.io | +| beamer | api.getbeamer.com | +| beebole | beebole-apps.com, beebole.com | +| beehiiv | api.beehiiv.com, www.blog.com | +| bettermode | api.bettermode.com, api.bettermode.de, developers.bettermode.com | +| bexio | api.bexio.com, auth.bexio.com | +| bigcommerce | api.bigcommerce.com, www.bigcommerce.com | +| bigin-by-zoho | accounts.zoho.com, accounts.zoho.com.au, accounts.zoho.com.cn, accounts.zoho.eu, accounts.zoho.in, accounts.zoho.jp, accounts.zoho.sa, accounts.zohocloud.ca, www.zohoapis.ca, www.zohoapis.com, www.zohoapis.com.au, www.zohoapis.com.cn, www.zohoapis.eu, www.zohoapis.in, www.zohoapis.jp, www.zohoapis.sa | +| bika | bika.ai, bika.com | +| billplz | www.billplz-sandbox.com, www.billplz.com | +| binance | api.binance.com | +| bitly | api-ssl.bitly.com, bit.ly | +| bland-ai | api.bland.ai, app.bland.ai | +| blockscout | eth.blockscout.com | +| bluesky | bsky.app, bsky.social, cdn.bsky.app | +| bocha-search | api.bochaai.com, open.bochaai.com | +| bokio | api.bokio.se | +| bolna | api.bolna.ai, bolna-recordings-india.s3.ap-south-1.amazonaws.com, platform.bolna.ai | +| bonjoro | vimily.github.io, www.bonjoro.com | +| bookedin | api.bookedin.ai | +| box | account.box.com, api.box.com | +| brave-search | api.search.brave.com, brave.com | +| browse-ai | api.browse.ai | +| browserless | production-ams.browserless.io, production-lon.browserless.io, production-sfo.browserless.io, www.browserless.io | +| buffer | api.buffer.com, buffer.com, publish.buffer.com | +| bumpups | api.bumpups.com, bumpups.com | +| bursty-ai | app.burstyai.com | +| buttondown | api.buttondown.com, buttondown.com | +| cal-com | api.cal.com, meetco.daily.co | +| calendly | api.calendly.com, calendly.com | +| call-rounded | api.callrounded.com | +| camb-ai | camb.ai, client.camb.ai | +| campaign-monitor | api.createsend.com, www.campaignmonitor.com | +| canny | canny.io | +| canva | api.canva.com, www.canva.com | +| capsule-crm | api.capsulecrm.com, capsulecrm.com, cloud.activepieces.com | +| captain-data | api.captaindata.co, docs.captaindata.co | +| carbone | account.carbone.io, api.carbone.io | +| cashfree-payments | api.cashfree.com, payout-api.cashfree.com, payout-gamma.cashfree.com, sandbox.cashfree.com | +| certopus | api.certopus.com | +| chain-aware | enterprise.api.chainaware.ai | +| chainalysis-api | public.chainalysis.com | +| chaindesk | app.chaindesk.ai | +| chargekeep | angel.co, beta.chargekeep.com, crm.chargekeep.com, entryurl.com, googleplus.com, otherlink.com, twitter.com, www.alexa.com, www.angelist.com, www.bbb.org, www.calendly.com, www.classdoor.com, www.crunchbase.com, www.domain.com, www.followers.com, www.instagram.com, www.linkedin.com, www.myprofile.com, www.nav.com, www.opencorporates.com, www.otherlink.com, www.pinterest.com, www.refererurl.com, www.rss.com, www.trustpilot.com, www.yelp.com, www.youtube.com, zoom.com | +| chartly | api.chartly.dev | +| chat-aid | api.chataid.com, app.chataid.com | +| chat-data | api.chat-data.com | +| chatbase | www.chatbase.co | +| chatfly | backend.chatfly.co | +| chatling | api.chatling.ai, app.chatling.ai | +| chatnode | api.public.chatnode.ai | +| chatsistant | app.chatsistant.com | +| chatwoot | app.chatwoot.com, chatwoot.yourcompany.com | +| checkout | api.checkout.com, api.sandbox.checkout.com | +| chess-com | api.chess.com | +| circle | app.circle.so, dickinson.circledev.net, reynolds.circledev.net | +| clarifai | api.clarifai.com, clarifai.com, www.iana.org | +| claude | api.anthropic.com, console.anthropic.com, docs.anthropic.com | +| clearout | api.clearout.io, docs.clearout.io | +| clearoutphone | api.clearoutphone.io | +| clicdata | api.clicdata.com | +| clicksend | rest.clicksend.com | +| clickup | api.clickup.com, app.clickup.com, attachments-public.clickup.com, attachments.clickup.com | +| clockify | api.clockify.me | +| clockodo | my.clockodo.com | +| close | api.close.com, google.com, www.github.com, www.linkedin.com | +| cloudconvert | api.cloudconvert.com, cloudconvert.com, eu-central.api.cloudconvert.com, storage.cloudconvert.com, us-east.api.cloudconvert.com | +| cloudinary | api.cloudinary.com, res.cloudinary.com | +| cloutly | app.cloutly.com | +| coda | coda.io | +| cody | getcody.ai | +| cognito-forms | www.cognitoforms.com | +| cohere | api.cohere.com, dashboard.cohere.com | +| cometapi | api.cometapi.com | +| comfyicu | comfy.icu, img.comfy.icu, r2.comfy.icu | +| confluence | developer.atlassian.com, support.atlassian.com | +| connectuc | api.connectuc.io, api.example.com, auth.uc-technologies.com, www.test.com | +| constant-contact | api.cc.email, authz.constantcontact.com | +| contentful | api.contentful.com, www.contentful.com | +| contextual-ai | api.contextual.ai, contextual.ai | +| contiguity | api.contiguity.com | +| convertkit | api.convertkit.com, github.com, help.convertkit.com | +| copper | api.copper.com | +| copy-ai | api.copy.ai, copy.ai | +| crisp | api.crisp.chat, marketplace.crisp.chat | +| cryptolens | api.cryptolens.io, app.cryptolens.io | +| cursor | api.cursor.com, cursor.com, github.com | +| customer-io | api-eu.customer.io, api.customer.io, customer.io, fly.customer.io, track-eu.customer.io, track.customer.io | +| customgpt | app.customgpt.ai, docs.customgpt.ai | +| dappier | api.dappier.com, platform.dappier.com | +| dashworks | api.dashworks.ai, web.dashworks.ai | +| dataforb2b | api.dataforb2b.ai, app.dataforb2b.ai | +| datafuel | api.datafuel.dev, app.datafuel.dev, platform.openai.com | +| datocms | site-api.datocms.com | +| deepgram | api.deepgram.com, console.deepgram.com | +| deepl | api-free.deepl.com, api.deepl.com, www.deepl.com | +| deepseek | api.deepseek.com, platform.deepseek.com | +| deftform | deftform.com | +| denser-ai | denser.ai | +| descript | descriptapi.com, web.descript.com | +| detecting-ai | api.detecting-ai.com, detecting-ai.com | +| devin | api.devin.ai | +| digital-ocean | api.digitalocean.com, cloud.digitalocean.com | +| digital-pilot | api.digitalpilot.app | +| dimo | attestation-api.dimo.zone, auth.dimo.zone, console.dimo.org, device-definitions-api.dimo.zone, identity-api.dimo.zone, telemetry-api.dimo.zone, token-exchange-api.dimo.zone, vehicle-triggers-api.dimo.zone | +| discord | discord.com, zoom.us | +| discourse | discourse.yourinstance.com | +| docsbot | api.docsbot.ai, docsbot.ai | +| doctly | api.doctly.ai, doctly.ai | +| documentpro | api.documentpro.ai, app.documentpro.ai | +| documerge | app.documerge.ai | +| drip | api.getdrip.com, www.getdrip.com | +| dropbox | api.dropboxapi.com, content.dropboxapi.com, www.dropbox.com | +| drupal | www.drupal.org | +| dub | api.dub.co, app.dub.co, dub.sh, twitter.com | +| duckdb | duckdb.org | +| dumpling-ai | app.dumplingai.com | +| dust | dust.tt, eu.dust.tt | +| easy-peasy-ai | easy-peasy.ai | +| echowin | echo.win | +| eden-ai | api.edenai.run, app.edenai.run | +| editionguard | app.editionguard.com | +| elastic-email | api.elasticemail.com, help.elasticemail.com | +| elevenlabs | elevenlabs.io | +| emailit | api.emailit.com, app.emailit.com | +| emailoctopus | api.emailoctopus.com | +| enrichlayer | enrichlayer.com, facebook.com, linkedin.com, sg.linkedin.com, www.facebook.com, www.linkedin.com, x.com | +| esignatures | esignatures.com | +| eth-name-service | gateway.thegraph.com, thegraph.com | +| everhour | api.everhour.com | +| exa | api.exa.ai, dashboard.exa.ai | +| extracta-ai | api.extracta.ai | +| facebook-leads | developers.facebook.com, graph.facebook.com | +| facebook-pages | developers.facebook.com, graph.facebook.com | +| famulor | api.example.com, app.famulor.de, recordings.famulor.de | +| fathom | fathom.video | +| fathom-analytics | api.usefathom.com, app.usefathom.com | +| feathery | api.feathery.io, link-to-filled-file.com | +| feedhive | api.feedhive.com, www.facebook.com | +| figma | api.figma.com, www.figma.com | +| file-helper | developer.mozilla.org, nodejs.org | +| filetopdf | api.filetopdf.dev, filetopdf.dev | +| fillout-forms | api.fillout.com | +| fireberry | api.fireberry.com | +| firecrawl | api.firecrawl.dev, docs.firecrawl.dev, firecrawl.dev | +| fireflies-ai | api.fireflies.ai, fireflies.ai | +| flipando | api.flipando.com, flipando-backend.herokuapp.com | +| fliqr-ai | app.fliqr.ai | +| flow-parser | api.flowparser.one | +| folk | api.folk.app, app.folk.app | +| foreplay-co | public.api.foreplay.co, www.facebook.com | +| formbricks | app.formbricks.com | +| formitable | api.formitable.com | +| formstack | activepieces.formstack.com, files.formstack.com, www.formstack.com | +| fountain | api.fountain.com | +| fragment | api.onfragment.com, app.onfragment.com | +| frame | api.frame.com, api.frame.io, developer.frame.io | +| free-agent | api.freeagent.com | +| frill | api.frill.co, app.frill.co | +| front | api.example.com, api2.frontapp.com, dev.frontapp.com | +| gameball | api.gameball.co, help.gameball.co | +| gamma | gamma.app, public-api.gamma.app | +| gcloud-pubsub | console.cloud.google.com, pubsub.googleapis.com, www.googleapis.com | +| gender-api | gender-api.com | +| generatebanners | api.generatebanners.com, www.generatebanners.com | +| getresponse | api.getresponse.com, app.getresponse.com | +| ghostcms | test-publication.ghost.io, www.gravatar.com | +| giftbit | api-testbed.giftbit.com, api.giftbit.com | +| gistly | api-portal.gist.ly, api.gist.ly, gist.ly | +| github | api.github.com, avatars.githubusercontent.com, github.com | +| gitlab | gitlab.com, secure.gravatar.com | +| gladia | api.gladia.io, app.gladia.io | +| glide | api.glideapps.com | +| gmail | accounts.google.com, console.cloud.google.com, gmail.googleapis.com, oauth2.googleapis.com, support.google.com, www.googleapis.com | +| goodmem | api.goodmem.ai | +| google-bigquery | accounts.google.com, bigquery.googleapis.com, console.cloud.google.com, oauth2.googleapis.com, www.googleapis.com | +| google-calendar | accounts.google.com, console.cloud.google.com, developers.google.com, oauth2.googleapis.com, support.google.com, www.google.com, www.googleapis.com | +| google-cloud-storage | accounts.google.com, cloudresourcemanager.googleapis.com, oauth2.googleapis.com, pubsub.googleapis.com, www.googleapis.com | +| google-contacts | accounts.google.com, lh3.googleusercontent.com, oauth2.googleapis.com, people.googleapis.com, www.googleapis.com | +| google-docs | accounts.google.com, console.cloud.google.com, docs.google.com, docs.googleapis.com, oauth2.googleapis.com, support.google.com, www.googleapis.com | +| google-drive | accounts.google.com, console.cloud.google.com, developers.google.com, drive.google.com, oauth2.googleapis.com, support.google.com, www.googleapis.com | +| google-forms | accounts.google.com, console.cloud.google.com, forms.googleapis.com, oauth2.googleapis.com, support.google.com, www.googleapis.com | +| google-gemini | docs.cloud.google.com, generativelanguage.googleapis.com, makersuite.google.com | +| google-my-business | accounts.google.com, mybusiness.googleapis.com, mybusinessbusinessinformation.googleapis.com, oauth2.googleapis.com, www.googleapis.com | +| google-search | console.cloud.google.com, discoveryengine.googleapis.com | +| google-search-console | accounts.google.com, cloud.activepieces.com, console.cloud.google.com, oauth2.googleapis.com, searchconsole.googleapis.com, www.googleapis.com | +| google-sheets | accounts.google.com, console.cloud.google.com, developers.google.com, docs.google.com, oauth2.googleapis.com, sheets.googleapis.com, support.google.com, www.googleapis.com | +| google-slides | accounts.google.com, docs.google.com, oauth2.googleapis.com, slides.googleapis.com, www.googleapis.com | +| google-tasks | accounts.google.com, developers.google.com, oauth2.googleapis.com, tasks.googleapis.com, www.googleapis.com | +| google-vertexai | aiplatform.googleapis.com, console.cloud.google.com, www.googleapis.com | +| googlechat | accounts.google.com, oauth2.googleapis.com, www.googleapis.com | +| gptzero-detect-ai | api.gptzero.me, app.gptzero.me | +| granola | public-api.granola.ai | +| greenhouse | app.greenhouse.io, auth.greenhouse.io, boards.greenhouse.io, harvest.greenhouse.io, linkedin.com, zoom.us | +| greenpt | api.greenpt.ai | +| greip | greipapi.com | +| griptape | cloud.griptape.ai | +| grist | docs.getgrist.com, support.getgrist.com, team.getgist.com | +| grok-xai | api.x.ai, console.x.ai, x.ai | +| groq | api.groq.com | +| guidelite | api.guidelite.ai, docs.guidelite.ai | +| hackernews | hacker-news.firebaseio.com | +| harvest | api.harvestapp.com, id.getharvest.com | +| hashi-corp-vault | vault.example.com | +| hastewire | hastewire.com | +| heartbeat | api.heartbeat.chat | +| hedy | api.hedy.bot, eu-api.hedy.bot | +| help-scout | api.helpscout.net, secure.helpscout.net | +| heygen | api.heygen.com, upload.heygen.com | +| heymarket-sms | api.heymarket.com, app.heymarket.com | +| hootsuite | developer.hootsuite.com, platform.hootsuite.com, twitter.com | +| housecall-pro | api.housecallpro.com | +| hubspot | api.hubapi.com, app.hubspot.com, developers.hubspot.com, share.hsforms.com, www.hubspot.com | +| hugging-face | huggingface.co | +| hume-ai | platform.hume.ai | +| hunter | api.hunter.io, hunter.io | +| hystruct | api.hystruct.com | +| ibm-cognose | your-cognos-server.com | +| iloveapi | api.ilovepdf.com, developer.ilovepdf.com | +| image-router | api.imagerouter.io, imagerouter.io | +| imap | support.google.com | +| influencers-club | api-dashboard.influencers.club | +| insighto-ai | api.insighto.ai | +| insta-charts | api.instacharts.io | +| instabase | aihub.instabase.com, www.instabase.com, your-organization.instabase.com | +| instagram-business | developers.facebook.com, graph.facebook.com | +| instantly-ai | api.instantly.ai | +| instasent | api.instasent.com, dashboard.instasent.com, github.com | +| intruder | api.intruder.io | +| invoiceninja | invoice-ninja.readthedocs.io | +| jina-ai | api.jina.ai, deepsearch.jina.ai, eu-r-beta.jina.ai, eu-s-beta.jina.ai, jina.ai, r.jina.ai, s.jina.ai | +| jogg-ai | api.jogg.ai, res.jogg.ai | +| jotform | api.jotform.com, eu-api.jotform.com, hipaa-api.jotform.com | +| jungle-grid | api.junglegrid.dev | +| just-invoice | api.justinvoice.io | +| kallabot-ai | api.kallabot.com, api.twilio.com, kallabot-s3-amazon.com, kallabot.com | +| kapso | api.kapso.ai, app.kapso.ai | +| katana | api.katanamrp.com | +| kimai | demo.kimai.org | +| kizeo-forms | forms.kizeo.com | +| klaviyo | a.klaviyo.com, developers.klaviyo.com, www.klaviyo.com | +| klenty | api.klenty.com, app.klenty.com | +| klipy | api.klipy.com, klipy.com | +| knack | api.knack.com | +| knock | api.knock.app | +| ko-fi | ko-fi.com | +| krisp-call | api.twilio.com, app.krispcall.com | +| kudosity | api.transmitmessage.com, api.transmitsms.com | +| kustomer | api.kustomerapp.com | +| lead-connector | api.worldbank.org, marketplace.gohighlevel.com, services.leadconnectorhq.com | +| leap-ai | api.workflows.tryleap.ai, app.tryleap.ai | +| leexi | app.leexi.ai, public-api.leexi.ai | +| lemlist | api.lemlist.com | +| lemon-squeezy | api.lemonsqueezy.com, app.lemonsqueezy.com | +| letmepost | api.letmepost.dev, bsky.app | +| lets-calendar | panel.letscalendar.com | +| letta | cloud.letta.ai | +| lever | api.lever.co | +| lightfunnels | app.lightfunnels.com, services.lightfunnels.com | +| line | api.line.me | +| linear | avatars.githubusercontent.com | +| linka | angel.co, beta.linka.ai, crm.linka.ai, entryurl.com, googleplus.com, otherlink.com, twitter.com, www.alexa.com, www.angelist.com, www.bbb.org, www.calendly.com, www.classdoor.com, www.crunchbase.com, www.domain.com, www.followers.com, www.instagram.com, www.linkedin.com, www.myprofile.com, www.nav.com, www.opencorporates.com, www.otherlink.com, www.pinterest.com, www.refererurl.com, www.rss.com, www.trustpilot.com, www.yelp.com, www.youtube.com, zoom.com | +| linkedin | api.linkedin.com, learn.microsoft.com, www.linkedin.com | +| linkup | api.linkup.so | +| linkupapi | api.linkupapi.com, app.linkupapi.com, www.linkedin.com | +| livesession | api.livesession.io, app.livesession.io | +| llmrails | api.llmrails.com, console.llmrails.com | +| lobstermail | api.lobstermail.ai, lobstermail.ai | +| localai | localai.io | +| lofty | api.lofty.ai, api.lofty.com | +| logrocket | api.logrocket.com, app.logrocket.com | +| logsnag | api.logsnag.com | +| lokalise | api.lokalise.com | +| loops | app.loops.so | +| lucidya | api.lucidya.com, pbs.twimg.com | +| lusha | api.lusha.com | +| luxury-presence | api.luxurypresence.com | +| magical-api | gw.magicalapi.com | +| magicslides | ...png, api.magicslides.app, www.magicslides.app | +| mailchain | app.mailchain.com | +| mailer-lite | connect.mailerlite.com, dashboard.mailerlite.com | +| mailercheck | app.mailercheck.com | +| maileroo | app.maileroo.com, smtp.maileroo.com, verify.maileroo.net | +| mailgun | api.eu.mailgun.net, api.mailgun.net, app.mailgun.com | +| mailjet | api.mailjet.com | +| manus | api.manus.ai, app.manus.ai, manus.im, s3.amazonaws.com | +| manychat | api.manychat.com, help.manychat.com | +| mastodon | mastodon.social | +| matomo | developer.matomo.org, matomo.example.com | +| mattermost | activepieces.mattermost.com | +| mautic | mautic.ddev.site | +| medullar | api.medullar.com, my.medullar.com | +| meetgeek-ai | api.meetgeek.ai, app.meetgeek.ai, meetgeek.ai | +| meistertask | www.meistertask.com, www.mindmeister.com | +| mem | api.mem.ai | +| mempool-space | mempool.space | +| messagebird | api.bird.com | +| metatext | api.metatext.ai, guard-api.metatext.ai | +| microsoft-teams-bot | github.com, graph.microsoft.com, graph.microsoft.us, learn.microsoft.com, login.microsoftonline.com, portal.azure.com | +| millionverifier | app.millionverifier.com, millionverifier.com | +| mind-studio | api.mindstudio.ai | +| mindee | api.mindee.net, platform.mindee.com | +| missive | mail.missiveapp.com, public.missiveapp.com | +| mistral-ai | api.mistral.ai, console.mistral.ai, gateway.ai.cloudflare.com | +| mixmax | api.mixmax.com, developer.mixmax.com | +| mixpanel | api.mixpanel.com | +| modelslab | modelslab.com | +| mollie | api.mollie.com, docs.mollie.com, webshop.example.org, www.mollie.com | +| monday | api.monday.com | +| moonclerk | api.moonclerk.com, app.moonclerk.com | +| mooninvoice | www.mooninvoice.com | +| motion | api.usemotion.com, app.usemotion.com | +| motiontools | api.motiontools.io, help.motiontools.io | +| moveo-ai | api.moveo.ai | +| moxie-crm | clients.domain.com, hello.hecticapp.dev, hello.withmoxie.dev, lh3.googleusercontent.com, meet.google.com, struxture-www-assets.s3.us-east-2.amazonaws.com, t3.gstatic.com, tel.meet, www.google.com | +| muna-ai | api.muna.ai, muna.ai | +| murf-api | api.murf.ai, murf.ai | +| mycase-piece | auth.mycase.com, external-integrations.mycase.com, mycaseapi.stoplight.io, www.acmecorp.com, www.mycase.com | +| mysendingbox | api.mysendingbox.fr, app.mysendingbox.fr | +| netlify | 5d7725b654c02c0007350e8a--my-site.netlify.app, 5d7725b654c02c0007350e8b--my-site.netlify.app, 5d7725b654c02c0007350e8c--my-site.netlify.app, api.netlify.com, app.netlify.com, cloud.activepieces.com, github.com, my-site.netlify.app | +| neverbounce | api.neverbounce.com, neverbounce.com | +| nifty | nifty.pm, niftypm.com, openapi.niftypm.com | +| ninjapipe | www.ninjapipe.app | +| ninox | api.ninox.com, ninox.com | +| notion | api.notion.com, www.notion.so | +| ntfy | docs.ntfy.sh | +| nuelink | nuelink.com | +| octopush-sms | api.octopush.com | +| odoo | www.odoo.com | +| omni-co | blobsrus.omniapp.co, data.iana.org, docs.aws.amazon.com | +| omnihr | api-docs.omnihr.co, api.omnihr.co | +| omnisend | api.omnisend.com, app.omnisend.com | +| oncehub | api.oncehub.com, oncehub.com | +| oneclickimpact | api.1clickimpact.com | +| onfleet | onf.lt, onfleet.com | +| open-phone | api.openphone.com | +| open-router | openrouter.ai | +| openai | api.openai.com, beta.openai.com, platform.openai.com | +| openmic-ai | api.openmic.ai, app.openmic.ai, docs.openmic.ai | +| opnform | api.opnform.com, opnform.com | +| opportify | api.opportify.ai, app.opportify.ai | +| oracle-database | download.oracle.com, ftp.debian.org, github.com, packages.debian.org | +| orimon | bot.orimon.ai, channel-connector.orimon.ai, orimon.ai, orimon.gitbook.io | +| outseta | documenter.getpostman.com | +| paddle | api.paddle.com, sandbox-api.paddle.com | +| pagerduty | api.pagerduty.com, your-subdomain.pagerduty.com | +| pandadoc | api.pandadoc.com | +| paperform | api.paperform.co | +| parallel | api.parallel.ai, platform.parallel.ai | +| parser-expert | api.parser.expert | +| parseur | api.parseur.com, app.parseur.com | +| pastebin | pastebin.com | +| pastefy | pastefy.app | +| paywhirl | api.paywhirl.com | +| pdf | github.com, sirv.com, stackoverflow.com | +| pdf-co | api.pdf.co, app.pdf.co | +| pdf4me | api.pdf4me.com, dev.pdf4me.com | +| pdfcrowd | api.pdfcrowd.com, pdfcrowd.com | +| pdfmonkey | api.pdfmonkey.io, dashboard.pdfmonkey.io, files.pdfmonkey.io, pdfmonkey.s3.eu-west-1.amazonaws.com | +| peekshot | api.peekshot.com, dashboard.peekshot.com | +| pendo | app.pendo.io | +| perplexity-ai | api.perplexity.ai, docs.perplexity.ai, www.perplexity.ai | +| personal-ai | api.personal.ai | +| phantombuster | api.phantombuster.com, hub.phantombuster.com, phantombuster.com | +| phone-validator | api.phonevalidator.com | +| photoroom | sdk.photoroom.com | +| pinch-payments | api.getpinch.com.au, auth.getpinch.com.au, web.getpinch.com.au | +| pinterest | api.pinterest.com, i.pinimg.com, www.pinterest.com | +| pipedrive | api.pipedrive.com, oauth.pipedrive.com, pipedrive-files.s3-eu-west-1.amazonaws.com, pipedrive.readme.io, pipedrive.zoom.us | +| placid | api.placid.app, placid.app | +| plausible | plausible.io | +| plunk | next-api.useplunk.com | +| pocketbase | pocketbase.your-project.com, your-host | +| podio | api.podio.com, podio.com | +| pollybot-ai | pollybot.app | +| polydoc | api.podio.com, api.polydoc.tech | +| poper | app.poper.ai | +| posthog | eu.i.posthog.com, eu.posthog.com, posthog.mycompany.com, us.i.posthog.com, us.posthog.com | +| postiz | api.postiz.com, x.com | +| postmark | api.postmarkapp.com | +| predict-leads | predictleads.com, www.onetonline.org | +| predis-ai | brain.predis.ai | +| presentation | api.presenton.ai, images.pexels.com, presenton.ai | +| productboard | api.productboard.com, app.productboard.com | +| produktly | api.produktly.com, app.example.com, produktly.com | +| promotekit | www.promotekit.com | +| prompthub | app.prompthub.us | +| promptmate | api.promptmate.io | +| provenexpert | www.provenexpert.com | +| proxycurl | nubela.co, www.linkedin.com | +| pubrio | api.pubrio.com, linkedin.com, logo.clearbit.com | +| pushbullet | api.pushbullet.com, www.pushbullet.com | +| pushover | api.pushover.net | +| pylon | api.usepylon.com | +| qawafel | core.development.qawafel.dev, core.qawafel.sa, qawafel.sa | +| qdrant | cloud.qdrant.io, qdrant.tech | +| quickbase | api.quickbase.com | +| quickbooks | appcenter.intuit.com, oauth.platform.intuit.com, quickbooks.api.intuit.com, sandbox-quickbooks.api.intuit.com | +| quickzu | app.quickzu.com | +| quizell | api.quizell.com, docs.quizell.com, quizell.com | +| qwilr | api.qwilr.com | +| raia-ai | api.raia2.com | +| raindrop | api.raindrop.io, raindrop.io | +| rapidtext-ai | app.rapidtextai.com | +| razorpay | api.razorpay.com | +| reachinbox | api.reachinbox.ai | +| readwise | readwise.io | +| recall-ai | ap-northeast-1.recall.ai, eu-central-1.recall.ai, us-east-1.recall.ai, us-west-2.recall.ai, zoom.us | +| recurly | app.recurly.com, v3.recurly.com | +| reddit | oauth.reddit.com, www.reddit.com | +| rendex | api.rendex.dev | +| reoon-verifier | emailverifier.reoon.com | +| reply-io | api.reply.io, linkedin.com | +| resend | api.resend.com, resend.com | +| respond-io | api.respond.io, cdn.chatapi.net | +| retable | api.retable.io | +| retell-ai | api.retellai.com | +| retune | retune.so | +| returning-ai | dev.returning.ai, integration.returning.ai, playground-integration.returning.ai, sgtr-integration.returning.ai, staging-integration.returning.ai | +| robolly | api.robolly.com, robolly.com | +| roe-ai | api.roe-ai.com | +| rss | dev.to, purl.org, res.cloudinary.com, www.w3.org | +| runware | my.runware.ai | +| saastic | api.saastic.com, saastic.com | +| salesloft | api.salesloft.com | +| sardis | api.sardis.sh, sardis.sh | +| savvycal | api.savvycal.com, savvycal.com, zoom.us | +| scenario | api.cloud.scenario.com, docs.scenario.com | +| scrapegrapghai | api.scrapegraphai.com, scrapegraphai.com | +| scrapeless | api.scrapeless.com, app.scrapeless.com | +| seek-table | www.seektable.com | +| send-it | sendit.infiniteappsai.com, www.linkedin.com | +| sender | api.sender.net | +| sendfox | api.sendfox.com, sendfox.com | +| sendgrid | api.eu.sendgrid.com, api.sendgrid.com | +| sendinblue | api.sendinblue.com | +| sendpulse | api.sendpulse.com | +| sendr | api.sendr.io, app.sendr.io, cdn.sendr.io, pages.sendr.io, sendr.io | +| senja | api.senja.io, cdn.senja.io, google.com | +| serp-api | serpapi.com | +| serpstat | api-docs.serpstat.com, api.serpstat.com | +| sessions-us | api.app.sessions.us, worldtimeapi.org | +| seven | app.seven.io, gateway.seven.io | +| shippo | api.goshippo.com, shippo-delivery.s3.amazonaws.com, tools.usps.com | +| short-io | api.short.io, statistics.short.io | +| sign-now | api.signnow.com, app.signnow.com, your-callback-url.com | +| signrequest | signrequest.com | +| simplepdf | cdn.simplepdf.eu, simplepdf.eu | +| simpliroute | api.simpliroute.com | +| simplybookme | user-api-v2.simplybook.me, user-api.simplybook.me | +| simplyprint | 169.254.169.254, cdn.simplyprint.io, files.simplyprint.io, simplyprint.io, www.gravatar.com | +| sitespeakai | api.sitespeak.ai, sitespeak.ai | +| skyprep | api.skyprep.io | +| skyvern | api.skyvern.com, app.skyvern.com | +| slack | a.slack-edge.com, api.slack.com, emoji.slack-edge.com, slack.com | +| slashed | venc.slashed.cloud | +| slidespeak | api.slidespeak.co, app.slidespeak.co, slidespeak-files.s3.us-east-2.amazonaws.com | +| slite | api.slite.com | +| smartlead | server.smartlead.ai | +| smartsheet | api.smartsheet.com | +| smartsuite | app.smartsuite.com, github.com, webhooks.smartsuite.com | +| smoove | lp.smoove.io, rest.smoove.io | +| smsmode | rest.smsmode.com | +| socialkit | api.socialkit.dev, www.youtube.com, youtube.com | +| softr | studio-api.softr.io, studio.softr.io, tables-api.softr.io | +| sofya | sofya.co | +| sperse | angel.co, app.sperse.com, beta.sperse.com, entryurl.com, googleplus.com, otherlink.com, testadmin.sperse.com, twitter.com, www.alexa.com, www.angelist.com, www.bbb.org, www.calendly.com, www.classdoor.com, www.crunchbase.com, www.domain.com, www.followers.com, www.instagram.com, www.linkedin.com, www.myprofile.com, www.nav.com, www.opencorporates.com, www.otherlink.com, www.pinterest.com, www.refererurl.com, www.rss.com, www.trustpilot.com, www.yelp.com, www.youtube.com, zoom.com | +| splitwise | secure.splitwise.com | +| spotify | accounts.spotify.com, api.spotify.com, developer.spotify.com | +| square | connect.squareup.com | +| stability-ai | api.stability.ai, platform.stability.ai | +| straico | api.straico.com, platform.straico.com | +| strale | api.strale.io, strale.dev | +| streak | api.streak.com | +| stripe | api.stripe.com, buy.stripe.com, invoice.stripe.com, pay.stripe.com, stripe.com | +| supabase | supabase.com, your-project-ref.supabase.co | +| supadata | api.supadata.ai, dash.supadata.ai, supadata.ai | +| surveymonkey | api.surveymonkey.com | +| surveytale | app.surveytale.com | +| swarmnode | api.swarmnode.ai, app.swarmnode.ai | +| synthesia | api.synthesia.io, docs.synthesia.io | +| systeme-io | api.systeme.io | +| talkable | www.talkable.com | +| tally | api.tally.so, tally.so | +| tapfiliate | api.tapfiliate.com | +| tarvent | api.tarvent.com, gmail.com | +| taskade | taskade.com, www.taskade.com | +| tavily | api.tavily.com, tavily.com | +| teable | app.teable.ai, help.teable.ai | +| teamhood | api-yourtenant.teamhood.com, app.teamhood.com | +| teamleader | api.focus.teamleader.eu, cloud.activepieces.com, focus.teamleader.eu, marketplace.teamleader.eu | +| telegram-bot | api.telegram.org, core.telegram.org, telegram.me | +| telnyx | api.telnyx.com | +| tenzo | api.gotenzo.com, auth.gotenzo.com | +| textcortex-ai | api.textcortex.com, textcortex.com | +| thankster | app.thankster.com | +| ticktick | api.ticktick.com, ticktick.com | +| tidely | api.tidely.com | +| tidycal | tidycal.com, zoom.us | +| time-ops | api.timeops.dk | +| timelines-ai | app.timelines.ai | +| tiny-talk-ai | api.tinytalk.ai, dashboard.tinytalk.ai | +| tl-dv | app.tldv.io, pasta.tldv.io, tldv.io | +| todoist | api.todoist.com, todoist.com | +| toggl-track | api.track.toggl.com, assets.track.toggl.com | +| trello | api.trello.com, trello.com | +| truelayer | api.truelayer.com, auth.truelayer.com | +| trust | api.usetrust.app, app.usetrust.io | +| twenty | app.twenty.com | +| twilio | api.twilio.com, demo.twilio.com, lookups.twilio.com | +| twin-labs | paris.prod.api.twin.so | +| twitch | api.twitch.tv, dev.twitch.tv, id.twitch.tv, static-cdn.jtvnw.net, twitch.tv, www.twitch.tv | +| twitter | developer.twitter.com | +| typeform | admin.typeform.com, api.typeform.com | +| typefully | api.typefully.com, support.typefully.com, typefully.com | +| umami | api.umami.is, cloud.umami.is, umami.example.com | +| upgradechat | angel.co, betacrm.upgrade.chat, crm.upgrade.chat, entryurl.com, googleplus.com, otherlink.com, twitter.com, www.alexa.com, www.angelist.com, www.bbb.org, www.calendly.com, www.classdoor.com, www.crunchbase.com, www.domain.com, www.followers.com, www.instagram.com, www.linkedin.com, www.myprofile.com, www.nav.com, www.opencorporates.com, www.otherlink.com, www.pinterest.com, www.refererurl.com, www.rss.com, www.trustpilot.com, www.yelp.com, www.youtube.com, zoom.com | +| uptimerobot | api.uptimerobot.com, dashboard.uptimerobot.com | +| uscreen | api.uscreen.io, uscreen.io | +| useinbox | app.inboxroad.com, app.useinbox.com, useapi.useinbox.com | +| uxsniff | api.uxsniff.com, app.uxsniff.com, google.com, uxsniff.com | +| vadoo-ai | ai.vadoo.tv, viralapi.vadoo.tv | +| validatedmails | api.validatedmails.com | +| valyu | api.valyu.ai, platform.valyu.ai | +| vapi | api.vapi.ai, dashboard.vapi.ai | +| vbout | api.vbout.com | +| vercel | api.vercel.com | +| vero | api.getvero.com, app.getvero.com | +| videoask | api.videoask.com, auth.videoask.com, media.videoask.com, media2.giphy.com, media3.giphy.com, www.videoask.com | +| vidlab7 | api-prd.vidlab7.com, studio.vidlab7.com | +| vidnoz | devapi.vidnoz.com | +| village | api.village.ai, linkedin.com | +| vimeo | api.vimeo.com, developer.vimeo.com, i.vimeocdn.com | +| visible | api.visible.vc | +| vlm-run | api.vlm.run | +| voipstudio | l7api.com, voipstudio.com | +| vouchery-io | admin.sandbox.vouchery.app | +| wafeq | api.wafeq.com, app.wafeq.com | +| waitwhile | api.waitwhile.com, v2.waitwhile.com, waitwhile.com | +| wealthbox | api.crmworkspace.com | +| webex | webexapis.com | +| webflow | api.webflow.com, webflow.com | +| webscraping-ai | api.webscraping.ai | +| wedof | community.n8n.io, test.wedof.fr, www.wedof.fr | +| week-done | api.weekdone.com, weekdone.com | +| weekdone | api.weekdone.com, weekdone.com | +| what-converts | app.whatconverts.com, www.whatconverts.com | +| whatsable | dashboard.whatsable.app | +| whatsapp | business.facebook.com, developers.facebook.com, graph.facebook.com | +| whatsscale | proxy.whatsscale.com, whatsscale.com | +| wistia | api.wistia.com, embed-ssl.wistia.com, my.wistia.com | +| wonderchat | app.wonderchat.io, wonderchat.io | +| woocommerce | myshop.com, mystore.com | +| woodpecker | api.woodpecker.co | +| wootric | api.wootric.com, assets-production.wootric.com | +| wrike | cloud.activepieces.com, login.wrike.com, www.wrike.com | +| writesonic-bulk | api.writesonic.com, app.writesonic.com | +| xero | api.xero.com, developer.xero.com, identity.xero.com, login.xero.com | +| xquik | docs.xquik.com, xquik.com | +| youcanbookme | api.youcanbook.me, app.youcanbook.me | +| youform | app.youform.com | +| youtube | accounts.google.com, cloud.activepieces.com, console.cloud.google.com, i4.ytimg.com, oauth2.googleapis.com, search.yahoo.com, www.googleapis.com, www.w3.org, www.youtube.com | +| zagomail | api.zagomail.com, app.zagomail.com | +| zendesk-sell | api.getbase.com | +| zeplin | api.zeplin.dev, placekitten.com, scene.zeplin.io | +| zerobounce | api.zerobounce.net | +| zoo | api.zoo.dev | +| zoom | api.zoom.us, marketplace.zoom.us, zoom.us | +| zuora | developer.zuora.com, knowledgecenter.zuora.com, rest.apisandbox.zuora.com, rest.eu.zuora.com, rest.na.zuora.com, rest.sandbox.eu.zuora.com, rest.sandbox.na.zuora.com, rest.test.eu.zuora.com, rest.test.zuora.com, rest.zuora.com | + +--- + +## 2. Dynamic hostnames + +These pieces build their hostname at runtime from a connection value. Split into two groups: **A** — the placeholder is a dropdown with a finite, known set of values, so the real hostnames are fully enumerable below; **B** — the placeholder is a freeform value only the customer knows (their account/org name, tenant ID, API-key region, …), but the vendor domain suffix is fixed, so ask the customer for the value and allowlist `.` (or request a wildcard on the fixed suffix if your infra process allows it). + +### 2A. Resolvable — enumerated real hostnames + +| Piece | Placeholder | Real hostnames | +|---|---|---| +| `amazon-bedrock` | `${service}.${region}.amazonaws.com` — service is one of 2 fixed values, region one of 20 AWS regions | `bedrock..amazonaws.com` and `bedrock-runtime..amazonaws.com` for region in: `us-east-1`, `us-east-2`, `us-west-2`, `ap-south-1`, `ap-south-2`, `ap-northeast-1`, `ap-northeast-2`, `ap-northeast-3`, `ap-southeast-1`, `ap-southeast-2`, `ca-central-1`, `eu-central-1`, `eu-central-2`, `eu-north-1`, `eu-south-1`, `eu-south-2`, `eu-west-1`, `eu-west-2`, `eu-west-3`, `sa-east-1` (40 hostnames total; narrow to the region(s) the customer actually uses if known) | +| `intercom` | region dropdown: US / EU / AU | `app.intercom.com`, `api.intercom.io` (US); `app.eu.intercom.com`, `api.eu.intercom.io` (EU); `app.au.intercom.com`, `api.au.intercom.io` (AU) | +| `microsoft-365-people`, `microsoft-365-planner`, `microsoft-copilot`, `microsoft-excel-365`, `microsoft-onedrive`, `microsoft-onenote`, `microsoft-outlook`, `microsoft-outlook-calendar`, `microsoft-sharepoint`, `microsoft-teams`, `microsoft-teams-bot`, `microsoft-todo` | Cloud Environment dropdown: Commercial / US Government | Commercial: `login.microsoftonline.com`, `graph.microsoft.com`. US Gov (GCC High): `login.microsoftonline.us`, `graph.microsoft.us`. **`microsoft-sharepoint` and `microsoft-copilot` additionally hit a per-tenant `.sharepoint.com`** (shown in Section 1 as the placeholder example `contoso.sharepoint.com`) — that part is freeform, ask the customer for their SharePoint tenant name. | +| `microsoft-dynamics-365-business-central` | fixed cloud API (`api.businesscentral.dynamics.com`, already in Section 1) plus an on-prem `${host}` path | Cloud: nothing extra needed. On-prem Business Central: fully customer-supplied host — treat like Section 3. | +| `microsoft-power-bi` | Cloud Environment dropdown | Commercial: `api.powerbi.com`, `analysis.windows.net` (already in Section 1). US Gov: `api.powerbigov.us` (already in Section 1). No extra action needed — both variants are already static-listed. | +| `zoho-mail`, `zoho-crm`, `zoho-desk`, `zoho-books`, `zoho-bookings`, `zoho-campaigns` | `location` dropdown, 6 data centers | Auth host `accounts.` for location in: `zoho.com` (US), `zoho.eu` (Europe), `zoho.in` (India), `zoho.com.au` (Australia), `zoho.jp` (Japan), `zohocloud.ca` (Canada). Product API host, same location value: `mail.` (zoho-mail), `desk.` (zoho-desk), `campaigns.` (zoho-campaigns), `www.zohoapis.` (zoho-crm, zoho-books, zoho-bookings — confirmed directly for bookings; crm/books read the equivalent `api_domain` back from Zoho's own OAuth response, which follows the same per-datacenter domain). | +| `zoho-invoice` | `region` dropdown, 5 data centers (no Canada) | Auth host `accounts.zoho.`, API host `www.zohoapis.` for region in: `com`, `eu`, `in`, `com.au`, `jp` | +| `coralogix` | `coralogixDomain` dropdown, 7 regions | Management API: `api.coralogix.com` (fixed, already in Section 1). Ingestion: `ingress.` for domain in: `eu1.coralogix.com`, `eu2.coralogix.com`, `us1.coralogix.com`, `us2.coralogix.com`, `ap1.coralogix.com`, `ap2.coralogix.com`, `ap3.coralogix.com` | +| `docusign` | `environment` dropdown: Demo/Test, US production, EU production | OAuth: `account-d.docusign.com` (demo) or `account.docusign.com` (production, both US and EU — the EU/US split happens after auth, not in the OAuth host). API: `demo.docusign.net` / `www.docusign.net` / `eu.docusign.net` matching the same selection | +| `datadog` | `site` dropdown, 7 regions (org subdomain is freeform, see 2B) | Site suffix is one of: `datadoghq.com`, `us3.datadoghq.com`, `us5.datadoghq.com`, `datadoghq.eu`, `ap1.datadoghq.com`, `ap2.datadoghq.com`, `ddog-gov.com`. Combine with the customer's org name (2B) as `.`, or just allowlist `api.datadoghq.com` (already in Section 1) if the piece only calls the plain REST API. | + +### 2B. Ask the customer for the value — fixed vendor suffix + +Pattern is `.` unless noted. Ask the customer for the value the way their own product surfaces it (usually visible in their account URL or settings page). + +| Piece | Real hostname pattern | What to ask the customer for | +|---|---|---| +| `algolia` | `.algolia.net` | Their Algolia Application ID (not secret, visible in the Algolia dashboard) | +| `backblaze` | `.`, endpoint typically `s3..backblazeb2.com` | Their exact B2 endpoint and bucket name (both shown on the bucket's details page) | +| `bubble` | `.bubbleapps.io` (or a mapped custom domain) | Their Bubble app name, or their custom domain if they mapped one | +| `cartloom` | `.cartloom.com` | Their Cartloom account/domain name | +| `chargebee` | `.chargebee.com` | Their Chargebee "site name" (Settings → Site Configuration) | +| `clickfunnels` | `.myclickfunnels.com` | Their ClickFunnels workspace subdomain | +| `fellow` | `.fellow.app` | Their Fellow workspace subdomain | +| `flowlu` | `.flowlu.com` | Their Flowlu account domain | +| `freshsales` | `.myfreshworks.com` | Their Freshsales/Freshworks account domain | +| `freshservice` | `.freshservice.com` | Their Freshservice account domain | +| `gorgias` | `.gorgias.com` | Their Gorgias account domain | +| `insightly` | `api..insightly.com` | Their Insightly "pod" — visible in their own API URL (e.g. `na1`, `eu1`); defaults to `na1` if unset | +| `kissflow` | `.` | Both values, from their Kissflow account settings | +| `kommo` | `.kommo.com` | Their Kommo account subdomain | +| `mailchimp` | `.api.mailchimp.com` | Derivable without asking: the datacenter suffix of their API key, after the final `-` (e.g. a key ending `-us21` → `us21`) | +| `netsuite` | `.suitetalk.api.netsuite.com` | Their NetSuite Account ID | +| `quaderno` | `.quadernoapp.com` (or `.sandbox-quadernoapp.com`) | Their Quaderno account name, and whether they use sandbox mode | +| `shopify` | `.myshopify.com` | Their Shopify store handle | +| `smaily` | `.sendsmaily.net` | Their Smaily account domain | +| `snowflake` | `.snowflakecomputing.com` | Their Snowflake Account Identifier (Account icon → View account details) | +| `teamwork` | `.teamwork.com` | Their Teamwork workspace subdomain | +| `workable` | `.workable.com` | Their Workable account subdomain | +| `workday` | tenant-specific host under `.workday.com` (pod varies — `wd2-`, `wd3-`, `wd5-…-impl-servicesN.workday.com`, etc.) | The exact API endpoint from their Workday tenant configuration — no fixed formula, Workday assigns pods per tenant | +| `wufoo` | `.wufoo.com` | Their Wufoo account subdomain | +| `zendesk` | `.zendesk.com` | Their Zendesk account subdomain | + +--- + +## 3. Arbitrary / self-hosted-server pieces + +These pieces don't call a vendor-owned domain at all — the connection's "server URL" field is passed straight through as the request host, so it can point anywhere the customer's own deployment lives (their own domain, an internal IP, a non-standard port). **There is no vendor suffix to wildcard.** For each of these, the only way to get a host to allowlist is to ask the customer directly for the exact URL they entered when creating the connection in Activepieces. + +| Piece | Auth field | Typical value (not enforced by code) | +|---|---|---| +| `wordpress` | Website URL | Self-hosted WordPress or wordpress.com site, entirely the customer's own domain | +| `gitea` | Base URL | Defaults to `gitea.com` if left blank; any self-hosted Gitea instance otherwise | +| `nocodb` | Base URL | Self-hosted NocoDB, or `app.nocodb.com` if using the hosted offering | +| `vtiger` | Instance URL | Self-hosted Vtiger CRM | +| `tableau` | Server URL | Tableau Server (on-prem) or a Tableau Cloud pod | +| `jira-data-center` | Instance URL | On-prem/Data Center Jira | +| `jira-cloud` | Instance URL | Nominally `.atlassian.net`, but not enforced — any URL is accepted | +| `mcp-client` | Server URL | Any MCP server the customer runs | +| `cyberark` | Server URL | Self-hosted CyberArk PVWA | +| `sap-ariba` | Base URL + OAuth Server URL | Commonly `api.ariba.com` / `openapi.ariba.com` per SAP's own convention, but both fields are freeform — confirm from the customer's Ariba "Environment details" page | +| `service-now` | Instance URL | Nominally `.service-now.com`, but not enforced | +| `coupa` | Instance URL | Nominally `.coupahost.com`, but not enforced | +| `microsoft-dynamics-crm` | Host URL | Nominally `.crm[N].dynamics.com`, but not enforced (on-prem Dynamics also possible). OAuth login host is separately fixed via the Microsoft Cloud Environment dropdown — see Section 2A. | +| `oracle-fusion-cloud-erp` | Server URL | Nominally `.fa..oraclecloud.com`, but not enforced | +| `brilliant-directories` | Site URL | Each customer's own directory site domain | +| `okta` | Domain | Nominally `.okta.com` or a mapped custom domain, but the code accepts literally anything typed in | +| `salesforce` | Environment | Two well-known values (`login.salesforce.com` production, `test.salesforce.com` sandbox) or the customer's My Domain (`.my.salesforce.com`) — but after OAuth, the piece actually calls the `instance_url` Salesforce hands back, which Salesforce itself assigns per-org and can differ from what was entered | +| `vtex` | Host URL | Nominally `.vtexcommercestable.com.br`, but not enforced | +| `webling` | Base URL | Nominally `.webling.ch`/`.de`/`.fr`, but not enforced | +| `sendy` | Domain | Self-hosted Sendy (self-hosted email sender) installation | +| `wayfront` | Workspace URL | Self-hosted Wayfront workspace | +| `fountain` | Base URL (optional) | Defaults to the fixed `api.fountain.com` (already covered in Section 1) — only becomes arbitrary if the customer sets a custom override | + +### Also worth flagging: self-hostable open-source integrations + +A number of pieces integrate with software that is commonly self-hosted even though Section 1 lists a SaaS/cloud default hostname for them. If the customer runs their own instance, treat these the same as the table above and ask for their real URL: `posthog` (default `us.posthog.com`/`us.i.posthog.com`, but has an explicit self-hosted base-URL override), `umami` (has a dedicated "Self-hosted" auth mode with an Instance URL field), `mattermost` (explicitly "open-source, self-hosted Slack alternative"), `mautic`, `chatwoot`, `discourse`, `matomo`, `ghostcms`, `gitlab` (self-hosted GitLab CE/EE, distinct from gitlab.com). + +--- + +*Generated 2026-08-03 by scanning `packages/pieces/community/*/src` and `packages/pieces/core/*/src` with `scripts/piece-hostnames.js`, cross-checked by reading each dynamic/arbitrary-URL piece's auth definition. Re-run the script (or extend it) after new pieces are added rather than hand-editing this file indefinitely — treat it as a snapshot, not a live source of truth.*