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
62 changes: 62 additions & 0 deletions .github/workflows/sync-model-catalog.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
name: Sync AI Model Catalog

on:
schedule:
- cron: '0 6 * * 1'
workflow_dispatch:

permissions:
contents: read

jobs:
sync:
if: github.repository == 'activepieces/activepieces'
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v5

- name: Cache dependencies
uses: actions/cache@v5
with:
path: ~/.bun/install/cache
key: bun-${{ hashFiles('bun.lock', 'package.json') }}
restore-keys: bun-

- name: Setup nodejs
uses: actions/setup-node@v6
with:
node-version: 24

- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: latest

- name: Install dependencies
run: bun install --frozen-lockfile

- name: Regenerate catalog
run: npm run sync-model-catalog

- name: Upload to CDN
env:
AWS_ACCESS_KEY_ID: ${{ secrets.CDN_S3_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.CDN_S3_SECRET_ACCESS_KEY }}
AWS_DEFAULT_REGION: us-east-1
AWS_REQUEST_CHECKSUM_CALCULATION: when_required
AWS_RESPONSE_CHECKSUM_VALIDATION: when_required
BUCKET: ${{ secrets.CDN_S3_BUCKET }}
ENDPOINT: ${{ secrets.CDN_S3_ENDPOINT }}
run: |
aws s3 cp dist/model-catalog.json \
"s3://$BUCKET/ai/model-catalog.json" \
--endpoint-url "$ENDPOINT" \
--content-type "application/json" \
--cache-control "max-age=3600" \
--acl public-read

- name: Verify publish
run: |
STATUS=$(curl -s -o /dev/null -w "%{http_code}" "https://cdn.activepieces.com/ai/model-catalog.json?run=$GITHUB_RUN_ID" || true)
[ "$STATUS" = "200" ] || { echo "Publish verification failed: HTTP $STATUS"; exit 1; }
45 changes: 44 additions & 1 deletion brain/knowledge/ai-intelligence/ai-providers.md

Large diffs are not rendered by default.

8 changes: 8 additions & 0 deletions brain/knowledge/connections-auth/ce-authentication.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,21 @@ The core (all-editions) auth layer: user identity creation, sign-in, and JWT ses
- First sign-up side effects: creates identity → User (PlatformRole.ADMIN) → default PERSONAL project; sends OTP on Cloud prod, auto-verifies otherwise; fires `USER_CREATED` flag + `SIGNED_UP` telemetry.
- **`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.

### Gotchas
- Email-auth checks and domain allow-listing guards are **skipped on Community** edition.
- **OTP verification is sent on Cloud unless `AP_ENVIRONMENT` is exactly `dev`, in which case the identity is silently auto-verified and no email goes out.** `sendVerificationOrAutoVerify` compares against `ApEnvironment.DEVELOPMENT`, whose value is the string **`dev`** — not `development`, which an earlier version of this line claimed. The distinction is not cosmetic: `AP_ENVIRONMENT=dev` on Cloud takes the `verify()` branch and the email-code flow becomes untestable locally, while any other value (including a typo like `development`, which fails the system validator with a warning and nothing more) falls through to `otpService.createAndSend` and really does email. So to exercise sign-up email locally on Cloud, `AP_ENVIRONMENT` must not be `dev`; `prod` also works but switches on the newsletter POST to a live endpoint. CE/EE take the other edition arm entirely.
- **`status: invalid` is deliberately NOT refused at sign-up.** A non-existent mailbox is allowed to create an (unverified) identity, because refusing it would make the public sign-up endpoint a mailbox-existence oracle for any address at any domain. So a random-address bot still gets a row; what it cannot get is a verified session. Do not "tighten" this without reading [000032](../decisions/000032-a-signup-address-is-checked-against-zerobounce-not-a-bundled-blocklist.md).
- **`do_not_mail` is not a rejection on its own.** It covers `role_based`, `role_based_catch_all` and `mx_forward` as well as the abuse sub-statuses, so refusing the whole status would refuse `info@` and `sales@` — normal ways a team signs up. Only four sub-statuses are refused: `disposable`, `toxic`, `possible_trap`, `global_suppression`. Note those four are **not** all the same shape: `disposable` is a property of the domain, while `toxic` and `global_suppression` (and `spamtrap`/`abuse`) describe one address. Any future caching or batching of verdicts has to respect that — a per-domain cache is sound only for `disposable`, and caching an *allow* verdict per domain is never sound, because it would skip the address-level checks for every other mailbox on that domain.
- **`turnstile.assertSolved` must stay ahead of the ZeroBounce call in `requestCode`.** Each validation costs a ZeroBounce credit, and the captcha is what stops an unsolved request from spending one. Reordering those two lines turns the endpoint into a way to drain the credit balance.
- **A refused address creates nothing, so a bot can re-hit the same address forever.** That used to cost a credit per attempt, which made draining the balance a bypass (fail-open means an empty balance passes everything). `disposable` verdicts are now cached in one `distributedStore` key, `zerobounce:disposable-domains:v1` — an insertion-ordered array of at most 500 domains, oldest dropped on overflow, no TTL — so repeat abuse on one domain costs a single credit fleet-wide. Rotating across *new* domains still costs a credit each — that half is bounded by the captcha, the auth rate limits and whatever the edge enforces, not by the cache.
- **ZeroBounce does not answer a bad key the way its docs say.** The documented failure is `HTTP 200` with `{"error": "Invalid API Key or your account ran out of credits"}`, and `isRefused` does check that body — but an unrecognised key is rejected at the Cloudflare edge with **`403` + `error code: 1020`**, on `api`/`api-us`/`api-eu` alike and for any User-Agent, so the axios-error branch is the one that fires. Both fail open, so the outcome is the same; what matters is that `1020` means "this key is not accepted", NOT "we are blocked". The block is scoped to the `api_key`-taking paths — `https://api.zerobounce.net/` answers `200` and `/v2/` answers `404` from the same host — so do not read a `1020` as a network or geo problem without checking those two first.
- **A mimicked response must match the real one down to value normalization.** `signUp`'s silent refusal echoes the address lowercased and trimmed, the way the identity service stores it. The first cut echoed it as submitted, so a mixed-case address came back verbatim on a refusal and lowercased on a real sign-up — a working oracle. The test asserts `toEqual` on the whole body, not just the code, which is what caught it.
- Telemetry PII (email/name) sent only on Cloud; CE/EE send non-PII fields (`pickTelemetryPii`). Sign-in telemetry covers password sign-in only, not SSO.
- Sessions are invalidated by rotating `tokenVersion` on `UserIdentity`.
- **An SMTP failure in `otpService.createAndSend` answers `500` *after* the identity row is committed.** The code path creates the identity, then sends; a rejected send throws out of the request, so the caller sees an error while a verified-nothing identity persists and no code exists for it. The failure also arrives with `[evlog] log.error() called after the wide event was emitted — Keys dropped: route, error`, so it never reaches observability either — meaning this is invisible in dashboards and only findable in raw container logs. Seen on a Cloud preview 2026-08-26; not fixed.
- **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.
Expand Down
Loading
Loading