Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions brain/knowledge/ai-intelligence/mcp-server.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@ icon: 🔌

Exposes an Activepieces project as an MCP server so AI clients (Claude Desktop, Cursor, Windsurf) can read and manipulate flows, connections, tables, and runs through a typed tool interface. One `McpServer` record per project (UNIQUE `projectId`), authenticated by a bearer token. Available in CE, EE, and Cloud.

### Vocabulary

**Grant** — one row of `mcp_oauth_token`: this user's live authorisation for one registered client. The unit the connect page lists and revokes, named `McpOAuthGrant` and served from `/v1/mcp-oauth/grants`.
**Client** — one `mcp_oauth_client` registration row. Not a stable identity: Claude Code and Codex re-run DCR per sign-in, so one client-as-a-product yields many rows, and one user re-authenticating yields many grants. _Avoid_: using "client" for the thing being revoked.
**Connection** — belongs to piece auth (`AppConnection`), never to MCP. _Avoid_: "MCP connection" in code; the tab label "Connections" and the `/mcp-server/connections` URL are deliberate copy, not the domain term — the code under `app/routes/mcp-server/grants/` says grant.

### Entities & services

- **McpServer** — per-project record: `id`, `projectId` (unique), `token` (72-char), `disabledTools[]` (JSONB, nullable; `null`/`[]` means all controllable tools enabled).
Expand Down
6 changes: 3 additions & 3 deletions brain/knowledge/connections-auth/ce-authentication.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ The core (all-editions) auth layer: user identity creation, sign-in, and JWT ses
- **`signUp` has two arms and only one of them can create a platform.** When `params.platformId` is set (self-hosted, or a custom domain) the member joins that existing platform through `getOrCreateWithProject` and no platform is ever created or named. When it is nil (Cloud only) the identity is created first, then `getPreferredPlatformId` looks for a platform the identity already belongs to; finding none it hands off to `authenticationUtils.provisionOrOnboard`, which creates the platform straight away when the identity already carries a name someone gave us, and only falls back to an ONBOARDING response (finished at `/create-platform`) when the name is the placeholder derived from the email. `getPreferredPlatformId` returns null on every non-Cloud edition. **The member never types a platform name; they type their own, and the platform name is derived from it.** `completeSignUp` takes a single `fullName` field (that is the whole of `CompleteSignUpRequest`) and calls `signupNames.platformNameFromSignup`, which prefers the company read off a work email domain (`"Activepieces"`) and falls back to the person (`"<FirstName>'s Platform"`, then the capitalised first token of the email local part, then `"My Platform"`). The project name follows from the platform name via `personalProjectName`.
- **ONBOARDING** is the pre-platform principal: `authenticationUtils.getOnboardingResponse` mints it with `platformId: null, projectId: null` for a verified identity that belongs to no platform yet **and whose name we only guessed**, so the member can call `POST /v1/platforms` (`securityAccess.unscoped([ONBOARDING, USER])`) and land on `/create-platform`. It is Cloud-only in practice, because on self-hosted `platformUtils.getPlatformIdForRequest` falls back to `getOldestPlatform()` and there is always a platform to join. `accessTokenManager.assertUserSession` still revalidates it against `tokenVersion` + `verified`.
- **Sign-up address validation** is one call to ZeroBounce (`zerobounce.maySignUp`), from `signUp` for the EMAIL provider and from `requestCode` for an address with no identity yet. It runs only when `AP_ZEROBOUNCE_API_KEY` is set, refuses the abuse half of `do_not_mail` plus `spamtrap`/`abuse`, and fails open on anything it cannot read. Both call sites refuse **silently**, and the lib throws nothing: `requestCode` returns the same `204` as a success (no identity, no code), and `signUp` throws `EMAIL_IS_NOT_VERIFIED`, the response a genuine unverified Cloud sign-up already produces. `DOMAIN_NOT_ALLOWED` is not used here at all. See [000032](../decisions/000032-a-signup-address-is-checked-against-zerobounce-not-a-bundled-blocklist.md).
- **Passwordless sign-in** (`EMAIL_LOGIN`) is a typed 6-digit code on the same OTP primitive, offered only when `ApFlagId.SMTP_CONFIGURED` is true, with password as the fallback path. See [000027](../decisions/000027-email-sign-in-is-a-typed-code-on-the-existing-otp-primitive.md) for the code-not-link, edition-reach and anti-enumeration reasoning.
- **Passwordless sign-in** (`EMAIL_LOGIN`) is a typed 6-digit code on the same OTP primitive, with password as the fallback path. Its two unauthenticated routes, `/otp/request` and `/otp/verify`, live in `passwordlessAuthModule`, registered **only in the Cloud arm of `app.ts` and only when `turnstile.isConfigured()`** — everywhere else they do not exist, so a self-hosted instance answers 404. `/complete-sign-up` stays in `authenticationModule` on every edition, because it finishes any ONBOARDING principal and not just a code sign-up. The UI reads one flag, `ApFlagId.EMAIL_CODE_AUTH_ENABLED`, which carries that same pair of conditions. See [000027](../decisions/000027-email-sign-in-is-a-typed-code-on-the-existing-otp-primitive.md) for the code-not-link and anti-enumeration reasoning and [000032](../decisions/000032-emailed-sign-in-codes-are-served-on-cloud-only-and-only-behind-a-captcha.md) for why the reach narrowed.

### Gotchas
- Email-auth checks and domain allow-listing guards are **skipped on Community** edition.
Expand All @@ -36,9 +36,9 @@ The core (all-editions) auth layer: user identity creation, sign-in, and JWT ses
- **A new unauthenticated endpoint must be added to `disallowedRoutes` in `packages/web/src/lib/api.ts`**, otherwise the SPA attaches whatever stale bearer token is still in storage and the call fails in exactly the situation the endpoint exists for.
- **The three signup guards in `authentication-utils.ts` differ in what they leak.** `assertEmailAuthIsEnabled` and `assertDomainIsAllowed` describe platform configuration, so surfacing their errors is safe. `assertUserIsInvitedToPlatformOrProject` describes one address, so surfacing it turns any public auth endpoint into an invitation oracle. All three are also inert unless `plan.ssoEnabled`.
- **We ask for a name only when we do not already have one, and `signupNames.isPlaceholderName` is what decides.** A name counts as a placeholder when the last name is empty *and* the first name matches `firstNameFromEmail` for that address case-insensitively — exactly what `requestCode` seeds an emailed-code identity with. Anything else provisions the platform without a second question, and the two other producers of a name cannot collide with the placeholder shape: `SignUpRequest` types `firstName`/`lastName` as `SAFE_STRING_PATTERN` (`^[^./]+$`, so an empty last name is a 400 at the schema, not just a required field in the form), and the Google callback substitutes `'john'`/`'doe'` when the provider omits a name. The comparison must stay case-insensitive: `requestCode` derives the name from the raw address while the identity stores it lowercased, so `AhmadTash@…` would otherwise look like a name its owner typed.
- **A nil `projectId` on the principal means "go to /create-platform" in four separate places.** Anything that mints a platform-less session has to satisfy all of them, not just the route guard.
- **A nil `platformId` on the response means "go to /create-platform" in four separate places** — `redirect.tsx`, `sign-in-form.tsx`, `sign-up-form.tsx` and the email-code step of `auth-drawer-body.tsx`. It used to be `projectId`, which stopped being a valid proxy once `getProjectAndToken` returned `projectId: null` instead of throwing: a provisioned member on a platform with `autoCreatePersonalProjects` off has a platform and no project, and the old test sent them to a name step whose `POST /complete-sign-up` rejects a USER token with 403. Anything that mints a platform-less session has to satisfy all four, not just the route guard.
- **Platform naming reads the email domain first, and "is this a work address" is a denylist of consumer brands.** `ahmad@activepieces.com` yields `"Activepieces"` while `ahmad@gmail.com` yields `"Ahmad's Platform"`. Two details are easy to get wrong when touching `signup-names.ts`. The denylist is keyed on the **registrable label**, not the full domain, so `yahoo.co.uk` is caught by the single entry `yahoo`. And the label is picked as the second-to-last domain part, stepping back one more when the part before the TLD is itself a public suffix (`co`, `com`, `ac`, ...), so `mail.activepieces.com`, `activepieces.co.uk` and `eu.activepieces.co.uk` all resolve to `Activepieces` rather than to `Mail`, `Co` or `Eu`. It is a heuristic, not a public-suffix list: a company sitting on an unlisted two-part suffix gets the suffix as its name. Only new signups are affected; existing platforms keep their names.
- **The route no longer decides sign-in vs sign-up — the card does.** `/sign-in`, `/sign-up` and `/create-platform` all render the same `AuthLanding`; `/sign-up` is a bare redirect to `/sign-in`. Which form you get is a function of two flags: with `SMTP_CONFIGURED` the card opens on the email-code step and the classic password form exists *only* behind the "Use password" link; without it you land on a password form directly, and `USER_CREATED` picks sign-up (first ever account, no mode switch offered) over sign-in. So the same URL renders three different DOMs across Cloud, a seeded self-host, and a fresh install — anything scripting this screen has to branch, and password sign-*up* is simply unreachable once SMTP is on.
- **The route no longer decides sign-in vs sign-up — the card does.** `/sign-in`, `/sign-up` and `/create-platform` all render the same `AuthLanding`; `/sign-up` is a bare redirect to `/sign-in`. Which form you get is a function of three flags: with `EMAIL_CODE_AUTH_ENABLED` and `SMTP_CONFIGURED` the card opens on the email-code step and the classic password form exists *only* behind the "Use password" link; without it you land on a password form directly, and `USER_CREATED` picks sign-up (first ever account, no mode switch offered) over sign-in. So the same URL renders three different DOMs across Cloud, a seeded self-host, and a fresh install — anything scripting this screen has to branch, and password sign-*up* is reachable behind it only when the card is in sign-up mode — the first-ever account, or an invitation link carrying `?email=`, since the passwordless method step renders no mode switch.
- **The sign-in URL's query string survives the email-code journey but not a federated one.** `/sign-up` forwards its search to `/sign-in`, and the card never navigates, so `?foo=bar` is still there at the end. Google/SAML instead do `window.location.href = …` and only `from`, `providerName` and `activepiecesLogin` ride along in the OAuth `state`; the customer returns on `/redirect` and goes to `from` or `/create-platform`. Anything that has to outlive sign-in for *every* provider belongs in `localStorage`, not in the URL.
- **`from` gets you back to the route but not to its query string — `AuthenticatedDefaultRoute` used to drop it.** Both `DefaultRoute` and `AllowOnlyLoggedInUserOnlyGuard` build `from` as `location.pathname + location.search`, so a param on the original URL survives sign-in and `useRedirectAfterLogin` navigates back to it. The last hop was where it died: landing on `/` authenticated renders `AuthenticatedDefaultRoute`, which navigated to `determineDefaultRoute(...)` with no `search`, so anything hanging off `/?x=1` was gone before the project routes (and the guards mounted inside them) rendered. That `Navigate` now forwards a single allow-listed param (`TRIAL_KEY_QUERY_PARAM`, in `route-utils.ts` beside `NEW_FLOW_QUERY_PARAM`), which is what lets a trial activation link reach the signed-in screen that consumes it. It deliberately does **not** forward the whole search string: `AuthenticatedDefaultRoute` also serves the `/*` catch-all, so blanket forwarding would push the query string of every unmatched URL into the default route for whatever page later sits there to read. A param that must survive that hop has to be added to the allow-list.
- **`/create-platform` is that same card opening on its name step**, off the ONBOARDING token rather than a route param — submitting the name is what mints the platform and project and swaps ONBOARDING for USER. The field is the *person's* `Full Name` (`data-testid="auth-full-name"`), not a workspace name. **Only the emailed-code path reaches it**: password sign-up and Google already collected a name, so those sessions are provisioned in the same request and land in the product with one form submission.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ Enterprise auth layer extending CE with SAML 2.0 SSO, Google/GitHub federated OA
- **RBAC**: `assertPrincipalAccessToProject({principal, permission, projectId})` and `assertUserHasPermissionToFlow` (maps FlowOperationType → Permission). Authorization hooks: `platformMustHaveFeatureEnabled` (402 FEATURE_DISABLED), `projectMustBeTeamType`, `platformMustBeOwnedByCurrentUser`.

### Gotchas
- **Until the passwordless work, CE could not send an OTP at all, despite the entity being registered for every edition.** `otpModule` was registered only in the CLOUD and ENTERPRISE arms of `app.ts`, and `emailService.sendOtp` returned early when the edition was neither. So on CE the table existed, the migration ran, and nothing could ever be sent. `EMAIL_LOGIN` changed that: `otpModule` is now registered for COMMUNITY too, and `EMAIL_LOGIN` is the one type carved out of the paid-edition send gate, so it reaches every edition while the UI gates it on `SMTP_CONFIGURED`. The two link types are still paid-edition only. RBAC base types are CE; **SSO, managed auth, federated OAuth are EE/Cloud only**.
- **Until the passwordless work, CE could not send an OTP at all, despite the entity being registered for every edition.** `otpModule` was registered only in the CLOUD and ENTERPRISE arms of `app.ts`, and `emailService.sendOtp` returned early when the edition was neither. So on CE the table existed, the migration ran, and nothing could ever be sent. `EMAIL_LOGIN` changed that: `otpModule` is now registered for COMMUNITY too, and `EMAIL_LOGIN` is the one type carved out of the paid-edition send gate, so the primitive reaches every edition. **The sign-in flow on top of it does not** — [000032](../decisions/000032-emailed-sign-in-codes-are-served-on-cloud-only-and-only-behind-a-captcha.md) put its three routes in a Cloud-only module that also needs a configured captcha, so the carve-out is currently unreachable and CE can still send nothing. The two link types are still paid-edition only. RBAC base types are CE; **SSO, managed auth, federated OAuth are EE/Cloud only**.
- **The public `POST /v1/otp` route deliberately cannot mint a login code.** Its `CreateOtpRequestBody` narrows `type` to `EMAIL_VERIFICATION | PASSWORD_RESET`, because that route is unauthenticated, carries no `rateLimit` config, and applies none of the sign-up guards. `EMAIL_LOGIN` is issued only through `POST /v1/authentication/otp/request`, which is rate limited and gated. Widening that enum back to the whole `OtpType` hands anyone an unthrottled "email a working sign-in code to this address" primitive.
- **A code sign-in must re-assert the platform's auth policy at verify time, not only at request time.** On Cloud `platformUtils.getPlatformIdForRequest` returns null for every unauthenticated request, so the request-scoped branch never runs there and the platform is only known after the identity is resolved. `verifyCode` therefore calls the same `assertEmailAuthIsEnabled` + `assertDomainIsAllowed` pair on the resolved preferred platform; without that, an email code signs a member into a platform that has deliberately disabled email auth or removed their domain. It is not asserted at request time on purpose, because reporting those errors for a resolved address would turn the request endpoint into an existence oracle.
- **`otpService.confirm` used to refresh its own resend lock.** `updated` is an `updateDate` column, so marking a row CONFIRMED touched it and the ten-minute guard then refused to issue that identity another code for ten minutes after a successful verify. Rows are deleted on confirm now.
Expand Down
2 changes: 1 addition & 1 deletion brain/knowledge/connections-auth/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ User identity, sign-in, JWT sessions. `UserIdentity` = canonical email+password+

### EE Authentication

Extends CE with SSO + RBAC. SAML 2.0 (`/v1/authn/saml/login` → IdP → ACS `/acs`) and Google/GitHub federated OAuth both funnel into `authenticationService.federatedAuthn()`; gated by `ssoEnabled`. Per-project RBAC via `assertPrincipalAccessToProject()` and `assertUserHasPermissionToFlow()`. Config stored on `platform.federatedAuthProviders`. Authz hooks: `platformMustHaveFeatureEnabled` (402), `projectMustBeTeamType`, `platformMustBeOwnedByCurrentUser`. OTP (email verify, password reset, and the `EMAIL_LOGIN` sign-in code) lives here. Its entity is registered for every edition, but `otpModule` is only registered on Cloud/EE and `sendOtp` returns early off those editions, so CE can send nothing today except `EMAIL_LOGIN`, which is gated on `SMTP_CONFIGURED` instead.
Extends CE with SSO + RBAC. SAML 2.0 (`/v1/authn/saml/login` → IdP → ACS `/acs`) and Google/GitHub federated OAuth both funnel into `authenticationService.federatedAuthn()`; gated by `ssoEnabled`. Per-project RBAC via `assertPrincipalAccessToProject()` and `assertUserHasPermissionToFlow()`. Config stored on `platform.federatedAuthProviders`. Authz hooks: `platformMustHaveFeatureEnabled` (402), `projectMustBeTeamType`, `platformMustBeOwnedByCurrentUser`. OTP (email verify, password reset, and the `EMAIL_LOGIN` sign-in code) lives here. Its entity is registered for every edition and `otpModule` now covers COMMUNITY too, but `sendOtp` returns early off Cloud/EE for the two link types and the `EMAIL_LOGIN` sign-in flow is served only on Cloud behind a configured captcha, so CE can send nothing today.

### Managed Auth / Embedding (EE)

Expand Down
Loading
Loading