From db40c637e10e8eaae67dd37fb6d8d7f3c56c21a7 Mon Sep 17 00:00:00 2001 From: LongYinan Date: Thu, 22 Oct 2026 18:49:33 +0800 Subject: [PATCH] feat(email): let administrators add outbound-only custom domains An apex whose MX belongs to Microsoft 365 or Google Workspace can now be registered with `void platform email domain-add --outbound-only`. The platform onboards only Cloudflare Email Sending and never touches the apex MX, Email Routing, catch-all or ingress Worker. - New `email_domain_routes.inbound_mode` column (managed | none), with rollback-safe compatibility entries for 1.1.11 through 1.2.1. - Outbound-only routes run sending_onboard + publish only, store inbound as disabled, and block (not verified_only) when Email Sending is refused. - A read-only Email Sending preview blocks setup with a checklist when existing DNS records conflict. - Admin only, zone apex only, never the shared mail zone. - The CLI refuses to continue when an older platform ignores the mode. Fixes #510 Co-Authored-By: Claude Opus 5.5 (1M context) --- docs/guide/email/domains.md | 2 +- docs/guide/platform/administration/email.md | 16 + docs/reference/cli/platform-email.md | 4 +- packages/void/src/cli/client.ts | 2 + packages/void/src/cli/email.ts | 10 +- packages/void/src/cli/operator-args.ts | 8 +- packages/void/src/cli/operator-cmd.ts | 12 + .../void/test/unit/email-cli-domain.test.ts | 17 +- packages/void/test/unit/operator-cli.test.ts | 46 + platform/meta/operations.md | 2 + .../api/drizzle/0061_email_inbound_mode.sql | 2 + .../packages/api/drizzle/compatibility.json | 16 + .../api/drizzle/meta/0061_snapshot.json | 6977 +++++++++++++++++ .../packages/api/drizzle/meta/_journal.json | 7 + .../packages/api/src/email/cf-customer.ts | 33 + .../api/src/email/connection-schema.ts | 9 + .../api/src/email/connection-service.ts | 202 +- .../packages/api/src/email/domain-service.ts | 63 +- platform/packages/api/src/operator/audit.ts | 1 + .../packages/api/src/operator/email-routes.ts | 54 +- .../test/integration/email-admission.test.ts | 66 + .../integration/email-connections.test.ts | 288 + .../test/integration/email-domain-cf-stub.ts | 9 + .../operator-email-domains.test.ts | 122 + .../unit/admin-upgrade-compatibility.test.ts | 56 + platform/packages/proxy/src/email.ts | 2 +- 26 files changed, 7951 insertions(+), 75 deletions(-) create mode 100644 platform/packages/api/drizzle/0061_email_inbound_mode.sql create mode 100644 platform/packages/api/drizzle/meta/0061_snapshot.json diff --git a/docs/guide/email/domains.md b/docs/guide/email/domains.md index 6e6eb8f62..7ca76bf20 100644 --- a/docs/guide/email/domains.md +++ b/docs/guide/email/domains.md @@ -16,7 +16,7 @@ void email domain add acme.com `add` opens a Cloudflare API-token template. Restrict the token to the selected account and mail zone before creating it; Workers Scripts permission applies across that account. The CLI accepts a masked paste or a newly copied token and asks you to confirm those restrictions. The platform encrypts the credential for the zone connection, so projects sharing that zone do not need separate ingress Workers. -Platform-managed custom inbound email requires the apex of a Cloudflare zone. Cloudflare’s catch-all only covers that apex, so a subdomain of the selected zone cannot receive arbitrary addresses. `add` refuses to register a new apex that already has MX records, or whose catch-all already routes elsewhere, so it never takes over mail hosted with another provider such as Microsoft 365 or Google Workspace. Use an apex that has no mail yet, or the platform’s shared mail address. Running `add` again for a registered apex, for example to renew its token, keeps working. Each custom apex belongs to one project; a project can register more than one apex. +Platform-managed custom inbound email requires the apex of a Cloudflare zone. Cloudflare’s catch-all only covers that apex, so a subdomain of the selected zone cannot receive arbitrary addresses. `add` refuses to register a new apex that already has MX records, or whose catch-all already routes elsewhere, so it never takes over mail hosted with another provider such as Microsoft 365 or Google Workspace. Use an apex that has no mail yet, or the platform’s shared mail address. To only send from an apex that keeps its current mail provider, ask your platform administrator to register it as [outbound-only](../platform/administration/email.md#sending-from-a-domain-that-already-receives-mail). Running `add` again for a registered apex, for example to renew its token, keeps working. Each custom apex belongs to one project; a project can register more than one apex. If setup is interrupted, use `void email domain status ` to inspect the saved operation before retrying. diff --git a/docs/guide/platform/administration/email.md b/docs/guide/platform/administration/email.md index b22a843ed..760d64426 100644 --- a/docs/guide/platform/administration/email.md +++ b/docs/guide/platform/administration/email.md @@ -150,3 +150,19 @@ Run recovery with the same platform version that created the persisted intent. `applied` continues without replaying the write; `not-applied` permits that exact step to retry. Secret rotation keeps its staged generation, and removal resumes from the unresolved cleanup step. Age alone never authorizes a retry. + +### Sending from a domain that already receives mail + +If an apex already receives mail with another provider, such as Microsoft 365 or Google Workspace, register it for sending only: + +```sh +void platform email domain-add brand.example --project hr-portal --outbound-only +``` + +The project can then send from any address on the apex, such as `support@brand.example`. It cannot receive mail there: the MX records, the apex SPF record, and incoming mail stay with the current provider. + +- Cloudflare Email Sending must be available, which needs Workers Paid. Without it, setup is blocked instead of falling back to verified destinations only. Upgrade, then run `domain-sync`. +- Email Sending adds its own records on `cf-bounce.` and `_dmarc.`. If Cloudflare reports that existing records conflict with them, for example several DMARC records, setup is blocked and the error lists each record. Fix them, then run `domain-sync`. +- Only administrators can register an outbound-only domain, and it must be a zone apex. The platform's shared mail zone cannot be outbound-only. +- `domain-rotate-secret` does not apply. `domain-remove` stops sending through the platform and leaves the MX records alone. +- To switch an outbound-only domain to platform-managed inbound mail, or back, remove it first and add it again. diff --git a/docs/reference/cli/platform-email.md b/docs/reference/cli/platform-email.md index 5cde91924..13ccceacc 100644 --- a/docs/reference/cli/platform-email.md +++ b/docs/reference/cli/platform-email.md @@ -66,14 +66,14 @@ Register and maintain email domains for projects whose owners hold no Cloudflare ```sh void platform email domains [--project ] -void platform email domain-add --project [--token-stdin] +void platform email domain-add --project [--token-stdin] [--outbound-only] void platform email domain-status void platform email domain-sync void platform email domain-rotate-secret void platform email domain-remove [--token-stdin] ``` -`domain-add` uses the platform's Cloudflare credential for zones in its account. For another account, pipe a scoped Cloudflare API token on standard input with `--token-stdin --yes`. Managed custom inbound email requires the Cloudflare zone apex (`example.com`). If its MX records already belong to another mail provider, use the project's shared email address or choose another unused zone. Native Cloudflare deployments also support literal recipient addresses on subdomains; see [Your own Cloudflare account](../../guide/email/domains.md#your-own-cloudflare-account). +`domain-add` uses the platform's Cloudflare credential for zones in its account. For another account, pipe a scoped Cloudflare API token on standard input with `--token-stdin --yes`. Managed custom inbound email requires the Cloudflare zone apex (`example.com`). If its MX records already belong to another mail provider, such as Microsoft 365 or Google Workspace, add `--outbound-only` to send from the apex while that provider keeps receiving its mail; see [Sending from a domain that already receives mail](../../guide/platform/administration/email.md#sending-from-a-domain-that-already-receives-mail). Otherwise use the project's shared email address or choose another unused zone. Native Cloudflare deployments also support literal recipient addresses on subdomains; see [Your own Cloudflare account](../../guide/email/domains.md#your-own-cloudflare-account). `domain-status` shows inbound, outbound, and credential-management readiness with the latest operation. A blocked operation resumes through `domain-sync`; a blocked rotation resumes through `domain-rotate-secret`, preserving already confirmed steps and its staged credential. An uncertain operation remains stopped until read-back proves the result or an administrator uses `operation-resolve`. Domains an administrator adds show `managed_by: admin`; their owners can list and inspect them but use these commands for `sync`, `domain-rotate-secret`, and `remove`. diff --git a/packages/void/src/cli/client.ts b/packages/void/src/cli/client.ts index 3213441f7..339ae2d7a 100644 --- a/packages/void/src/cli/client.ts +++ b/packages/void/src/cli/client.ts @@ -451,6 +451,8 @@ export type EmailDomainResponse = { domain: string; projectId: string; managed_by: 'owner' | 'admin'; + /** `none` sends only; the domain's MX provider keeps inbound mail. */ + inboundMode?: 'managed' | 'none'; status: EmailReadiness['inbound']['status']; connectionId: string; account_id: string; diff --git a/packages/void/src/cli/email.ts b/packages/void/src/cli/email.ts index 620e48199..0d209db2b 100644 --- a/packages/void/src/cli/email.ts +++ b/packages/void/src/cli/email.ts @@ -470,7 +470,7 @@ export async function emailDomainAdd( // A foreign MX on the apex stops here; an unreadable one is left to the platform, which // re-checks over public DNS. const dns = deps.dns ?? (await createNodeEmailDns()); - const checkApexMx = async (): Promise => { + const checkApexMx = async (adminProject?: string): Promise => { const mx = await probeMx(dns, domain); if (mx.state === 'unknown') { log.info( @@ -478,7 +478,11 @@ export async function emailDomainAdd( ); } else if (mx.state === 'foreign') { fail( - `email: ${domain} already receives mail from another provider. Platform custom inbound email requires an unused Cloudflare zone apex; choose another apex or use the platform shared mail address.`, + `email: ${domain} already receives mail from another provider. Platform custom inbound email requires an unused Cloudflare zone apex; choose another apex or use the platform shared mail address.${ + adminProject + ? ` To only send from ${domain}, ask your administrator to run \`void platform email domain-add ${domain} --project ${adminProject} --outbound-only\`.` + : '' + }`, ); } else { log.info(`MX using the requested apex ${domain}`); @@ -493,8 +497,8 @@ export async function emailDomainAdd( const rows = await client.listEmailDomains(projectId); renewal = rows.some((row) => row.domain === domain && row.managed_by !== 'admin'); if (!renewal) { - await checkApexMx(); const project = settings.domains.projectSlug ?? projectId; + await checkApexMx(project); log.info( [ 'Email domains on this platform are managed by its administrator.', diff --git a/packages/void/src/cli/operator-args.ts b/packages/void/src/cli/operator-args.ts index 27ac19835..d9e128c7c 100644 --- a/packages/void/src/cli/operator-args.ts +++ b/packages/void/src/cli/operator-args.ts @@ -496,7 +496,10 @@ export const operatorCommands: Array = [ path: '/email/domains', positional: ['domain'], values: { project: 'Project id or slug' }, - booleans: { 'token-stdin': 'Read a scoped Cloudflare API token from stdin' }, + booleans: { + 'token-stdin': 'Read a scoped Cloudflare API token from stdin', + 'outbound-only': 'Send only; leave the MX records and inbound mail with the current provider', + }, method: 'POST', }, { @@ -847,6 +850,9 @@ export function parseOperatorArgs(args: Array): OperatorArgs { parsed.body.protected = true; } else if (group === 'zero-trust' && action === 'project-public') { parsed.body.protected = false; + } else if (group === 'email' && action === 'domain-add' && flags.has('outbound-only')) { + delete parsed.body['outbound-only']; + parsed.body.outboundOnly = true; } return parsed; } diff --git a/packages/void/src/cli/operator-cmd.ts b/packages/void/src/cli/operator-cmd.ts index 05370d99b..266069b28 100644 --- a/packages/void/src/cli/operator-cmd.ts +++ b/packages/void/src/cli/operator-cmd.ts @@ -689,6 +689,18 @@ export async function runOperatorCommand(args: OperatorArgs): Promise { } recoveryFence = { recoveryFence: target.recoveryFence }; } + // An older platform ignores outboundOnly and would take over the domain's MX. + if ( + args.group === 'email' && + args.action === 'domain-add' && + args.body.outboundOnly === true && + (preview.target as Record | null)?.inboundMode !== 'none' + ) { + throw new OperatorClientError( + 'This platform does not support outbound-only email domains. Upgrade it first. No change was requested.', + 'unsupported', + ); + } if (!(await confirmOperatorMutation(origin, preview, args))) { return; } diff --git a/packages/void/test/unit/email-cli-domain.test.ts b/packages/void/test/unit/email-cli-domain.test.ts index d5399699c..47c4787ba 100644 --- a/packages/void/test/unit/email-cli-domain.test.ts +++ b/packages/void/test/unit/email-cli-domain.test.ts @@ -37,7 +37,7 @@ const LEGACY_TOKEN = `Lgcy-${'a1'.repeat(17)}B`; // 40 chars, [A-Za-z0-9_-] const CFUT_TOKEN = `cfut_${'c0'.repeat(20)}${'deed'.repeat(2)}`; // 40-char core + checksum tail /** The default platform: owners register their own domains. */ const selfServeSettings = async (): Promise<{ - domains: { management: 'self-serve' | 'admin' }; + domains: { management: 'self-serve' | 'admin'; projectSlug?: string }; }> => ({ domains: { management: 'self-serve' } }); const noEmailDomains = async (): Promise> => []; @@ -303,6 +303,21 @@ describe('emailDomainAdd', () => { expect(printed(infoSpy)).toContain('void platform email domain-add acme.com --project prj_1'); }); + it('points an administrator-managed apex with foreign mail at outbound-only setup', async () => { + const api = client(); + const deps = addDeps({ + dns: { resolveMx: async () => [{ exchange: 'aspmx.l.google.com', priority: 1 }] }, + }); + api.getEmailSettings.mockResolvedValue({ + domains: { management: 'admin', projectSlug: 'hr-portal' }, + }); + await expect(emailDomainAdd(api, 'prj_1', 'acme.com', deps)).rejects.toThrow('exit 1'); + expect(printed(errorSpy)).toContain( + 'void platform email domain-add acme.com --project hr-portal --outbound-only', + ); + expect(api.addEmailDomain).not.toHaveBeenCalled(); + }); + it('lets an owner renew their earlier assignment after the administrator mode switch', async () => { const api = client(); const deps = addDeps(); diff --git a/packages/void/test/unit/operator-cli.test.ts b/packages/void/test/unit/operator-cli.test.ts index 0fd36cc8b..89f2a1ce8 100644 --- a/packages/void/test/unit/operator-cli.test.ts +++ b/packages/void/test/unit/operator-cli.test.ts @@ -521,6 +521,19 @@ describe('operator argument and help coverage', () => { tokenStdin: true, yes: true, }); + const outboundOnly = parseOperatorArgs([ + 'email', + 'domain-add', + 'brand.example.com', + '--project', + 'hr-portal', + '--outbound-only', + ]); + expect(outboundOnly.body).toEqual({ + domain: 'brand.example.com', + project: 'hr-portal', + outboundOnly: true, + }); expect(parseOperatorArgs(['email', 'domain-status', 'mail.example.com'])).toMatchObject({ method: 'GET', path: '/email/domains/mail.example.com', @@ -1260,6 +1273,39 @@ describe('operator transport and execution', () => { await expect(runOperatorCommand(args)).rejects.toMatchObject({ code: 'invalid_preview' }); expect(fetcher).toHaveBeenCalledTimes(1); }); + it('stops an outbound-only add when the platform preview ignores the mode', async () => { + vi.stubEnv('VOID_API_URL', 'https://one.example'); + vi.stubEnv('VOID_OPERATOR_TOKEN', 'operator'); + vi.spyOn(console, 'log').mockImplementation(() => {}); + const preview = (target: Record) => + Response.json({ dryRun: true, action: 'email.domain.add', target }); + const fetcher = vi + .fn() + .mockResolvedValueOnce(preview({ domain: 'brand.example.com' })); + vi.stubGlobal('fetch', fetcher); + const args = parseOperatorArgs([ + 'email', + 'domain-add', + 'brand.example.com', + '--project', + 'hr-portal', + '--outbound-only', + '--yes', + ]); + await expect(runOperatorCommand(args)).rejects.toMatchObject({ code: 'unsupported' }); + expect(fetcher).toHaveBeenCalledTimes(1); + + fetcher + .mockReset() + .mockResolvedValueOnce(preview({ domain: 'brand.example.com', inboundMode: 'none' })) + .mockResolvedValueOnce(Response.json({ domain: { operationStatus: 'completed' } })); + await runOperatorCommand(args); + expect(JSON.parse(String(fetcher.mock.calls[1]![1]!.body))).toEqual({ + domain: 'brand.example.com', + project: 'hr-portal', + outboundOnly: true, + }); + }); it('drains runtime cursors and rejects repeated cursors instead of spinning', async () => { vi.spyOn(console, 'log').mockImplementation(() => {}); const fetcher = vi diff --git a/platform/meta/operations.md b/platform/meta/operations.md index 4dc61901b..0b06d397a 100644 --- a/platform/meta/operations.md +++ b/platform/meta/operations.md @@ -184,6 +184,8 @@ the exact stored mutation. Record `applied` to continue without replaying it, or `not-applied` to retry only that fenced intent. Keep the same platform version and preserve staged secret state until the operation completes. +Outbound-only domains (`domain-add --outbound-only`) need a release that knows the `inbound_mode` column. After a platform rollback to an earlier release, do not run `domain-add`, `domain-sync`, or `sync` for them: the older release treats them as normal domains and would enable Email Routing, which replaces the apex MX records and takes over the provider's mail. Sending keeps working without these commands. Upgrade again first. + PostgreSQL is authoritative for consent, policy, suspension, and recipient-attempt quotas. Cache publication retries independently. `void email logs` exposes receipt and action metadata: provider acceptance is not proof of delivery, and an unknown result must not be retried with a new idempotency key. Removing one domain or project must preserve sibling routes and unresolved cleanup records. Before production enablement, complete the [email acceptance checks](./release-acceptance.md#email-acceptance), including live Cloudflare routing, forwarding/reply, scoped-token, rotation, and failure-recovery checks. diff --git a/platform/packages/api/drizzle/0061_email_inbound_mode.sql b/platform/packages/api/drizzle/0061_email_inbound_mode.sql new file mode 100644 index 000000000..0019d5c7b --- /dev/null +++ b/platform/packages/api/drizzle/0061_email_inbound_mode.sql @@ -0,0 +1,2 @@ +ALTER TABLE "email_domain_routes" ADD COLUMN "inbound_mode" text DEFAULT 'managed' NOT NULL;--> statement-breakpoint +ALTER TABLE "email_domain_routes" ADD CONSTRAINT "email_domain_routes_inbound_mode_check" CHECK ("email_domain_routes"."inbound_mode" = 'managed' OR ("email_domain_routes"."inbound_mode" = 'none' AND "email_domain_routes"."addressing" = 'custom')); \ No newline at end of file diff --git a/platform/packages/api/drizzle/compatibility.json b/platform/packages/api/drizzle/compatibility.json index 753d3ffa5..0704fa5f8 100644 --- a/platform/packages/api/drizzle/compatibility.json +++ b/platform/packages/api/drizzle/compatibility.json @@ -81,6 +81,22 @@ { "platformVersion": "1.1.0", "schemaDigest": "bd1c4f124ee52cde71a9e2e974e3af2f1431cc7c92796be233784d5790b06eb4" + }, + { + "platformVersion": "1.1.11", + "schemaDigest": "d8e007d2bbe02637f6e77a33c1db152e0c4f21b30439ad512e16bce6726d6b9b" + }, + { + "platformVersion": "1.1.12", + "schemaDigest": "d8e007d2bbe02637f6e77a33c1db152e0c4f21b30439ad512e16bce6726d6b9b" + }, + { + "platformVersion": "1.2.0", + "schemaDigest": "d8e007d2bbe02637f6e77a33c1db152e0c4f21b30439ad512e16bce6726d6b9b" + }, + { + "platformVersion": "1.2.1", + "schemaDigest": "d8e007d2bbe02637f6e77a33c1db152e0c4f21b30439ad512e16bce6726d6b9b" } ] } diff --git a/platform/packages/api/drizzle/meta/0061_snapshot.json b/platform/packages/api/drizzle/meta/0061_snapshot.json new file mode 100644 index 000000000..6be8f5b55 --- /dev/null +++ b/platform/packages/api/drizzle/meta/0061_snapshot.json @@ -0,0 +1,6977 @@ +{ + "id": "90973306-0586-467f-88b6-6ebded4418eb", + "prevId": "7eac94a1-0534-4b0e-964c-7ac1085951f3", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.auth_codes": { + "name": "auth_codes", + "schema": "", + "columns": { + "code": { + "name": "code", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "jwt": { + "name": "jwt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "cli_state": { + "name": "cli_state", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "code_challenge": { + "name": "code_challenge", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "code_challenge_method": { + "name": "code_challenge_method", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.builds": { + "name": "builds", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "executor": { + "name": "executor", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'container'" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'queued'" + }, + "commit_sha": { + "name": "commit_sha", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "branch": { + "name": "branch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "deployment_id": { + "name": "deployment_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "github_run_id": { + "name": "github_run_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "github_run_attempt": { + "name": "github_run_attempt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "logs_url": { + "name": "logs_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "check_run_id": { + "name": "check_run_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "exit_code": { + "name": "exit_code", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "instance_type": { + "name": "instance_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_builds_project_created": { + "name": "idx_builds_project_created", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_builds_status": { + "name": "idx_builds_status", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_builds_user_status": { + "name": "idx_builds_user_status", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_builds_project_github_run_id": { + "name": "idx_builds_project_github_run_id", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "github_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "github_run_attempt", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"builds\".\"github_run_id\" IS NOT NULL AND \"builds\".\"github_run_attempt\" IS NOT NULL AND \"builds\".\"status\" IN ('starting', 'building', 'deploying')", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "builds_project_id_projects_id_fk": { + "name": "builds_project_id_projects_id_fk", + "tableFrom": "builds", + "tableTo": "projects", + "columnsFrom": ["project_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "builds_user_id_users_id_fk": { + "name": "builds_user_id_users_id_fk", + "tableFrom": "builds", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "builds_deployment_id_deployments_id_fk": { + "name": "builds_deployment_id_deployments_id_fk", + "tableFrom": "builds", + "tableTo": "deployments", + "columnsFrom": ["deployment_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.deployment_operations": { + "name": "deployment_operations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "build_id": { + "name": "build_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "original_active_id": { + "name": "original_active_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cli_version": { + "name": "cli_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "input_digest": { + "name": "input_digest", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "input_prefix": { + "name": "input_prefix", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'queued'" + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "lease_expires_at": { + "name": "lease_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "next_dispatch_at": { + "name": "next_dispatch_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "events": { + "name": "events", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "last_seq": { + "name": "last_seq", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "inputs_deleted": { + "name": "inputs_deleted", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_deployment_operations_key": { + "name": "idx_deployment_operations_key", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "actor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_deployment_operations_dispatch": { + "name": "idx_deployment_operations_dispatch", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "next_dispatch_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "deployment_operations_project_id_projects_id_fk": { + "name": "deployment_operations_project_id_projects_id_fk", + "tableFrom": "deployment_operations", + "tableTo": "projects", + "columnsFrom": ["project_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.deployments": { + "name": "deployments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "manifest": { + "name": "manifest", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "commit": { + "name": "commit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "repository_url": { + "name": "repository_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "commit_url": { + "name": "commit_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "git_ref": { + "name": "git_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cli_version": { + "name": "cli_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "admission_protocol": { + "name": "admission_protocol", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "asset_hashes": { + "name": "asset_hashes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deployed_at": { + "name": "deployed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "retained": { + "name": "retained", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "applied_migrations": { + "name": "applied_migrations", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bundle_stored": { + "name": "bundle_stored", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "billing_user_id": { + "name": "billing_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "initiated_by_user_id": { + "name": "initiated_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "proxy_token_gen": { + "name": "proxy_token_gen", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": { + "idx_deployments_project_id": { + "name": "idx_deployments_project_id", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_deployments_project_status": { + "name": "idx_deployments_project_status", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_deployments_created_id": { + "name": "idx_deployments_created_id", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "deployments_project_id_projects_id_fk": { + "name": "deployments_project_id_projects_id_fk", + "tableFrom": "deployments", + "tableTo": "projects", + "columnsFrom": ["project_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.domains": { + "name": "domains", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "hostname": { + "name": "hostname", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "cf_hostname_id": { + "name": "cf_hostname_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "verification_data": { + "name": "verification_data", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_domains_project_id": { + "name": "idx_domains_project_id", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "domains_project_id_projects_id_fk": { + "name": "domains_project_id_projects_id_fk", + "tableFrom": "domains", + "tableTo": "projects", + "columnsFrom": ["project_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "domains_hostname_unique": { + "name": "domains_hostname_unique", + "nullsNotDistinct": false, + "columns": ["hostname"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.email_destination_cleanups": { + "name": "email_destination_cleanups", + "schema": "", + "columns": { + "cf_destination_id": { + "name": "cf_destination_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "next_attempt_at": { + "name": "next_attempt_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "lease_started_at": { + "name": "lease_started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_email_destination_cleanups_due": { + "name": "idx_email_destination_cleanups_due", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "next_attempt_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "email_destination_cleanups_status_check": { + "name": "email_destination_cleanups_status_check", + "value": "\"email_destination_cleanups\".\"status\" IN ('pending', 'deleting', 'completed')" + }, + "email_destination_cleanups_attempts_check": { + "name": "email_destination_cleanups_attempts_check", + "value": "\"email_destination_cleanups\".\"attempts\" >= 0" + } + }, + "isRLSEnabled": false + }, + "public.email_destinations": { + "name": "email_destinations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "address": { + "name": "address", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "cf_destination_id": { + "name": "cf_destination_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "verified_at": { + "name": "verified_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "verification_source": { + "name": "verification_source", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'cf'" + }, + "verified_by_user_id": { + "name": "verified_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "verified_by_email_snapshot": { + "name": "verified_by_email_snapshot", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_email_destinations_project_id": { + "name": "idx_email_destinations_project_id", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_email_destinations_cf_destination_id": { + "name": "idx_email_destinations_cf_destination_id", + "columns": [ + { + "expression": "cf_destination_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_email_destinations_verified_by_user_id": { + "name": "idx_email_destinations_verified_by_user_id", + "columns": [ + { + "expression": "verified_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "verified_by_email_snapshot", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "email_destinations_project_id_projects_id_fk": { + "name": "email_destinations_project_id_projects_id_fk", + "tableFrom": "email_destinations", + "tableTo": "projects", + "columnsFrom": ["project_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "email_destinations_verified_by_user_id_users_id_fk": { + "name": "email_destinations_verified_by_user_id_users_id_fk", + "tableFrom": "email_destinations", + "tableTo": "users", + "columnsFrom": ["verified_by_user_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "email_destinations_project_address_uq": { + "name": "email_destinations_project_address_uq", + "nullsNotDistinct": false, + "columns": ["project_id", "address"] + } + }, + "policies": {}, + "checkConstraints": { + "email_destinations_status_check": { + "name": "email_destinations_status_check", + "value": "\"email_destinations\".\"status\" IN ('pending', 'verified')" + }, + "email_destinations_verification_source_check": { + "name": "email_destinations_verification_source_check", + "value": "\"email_destinations\".\"verification_source\" IN ('cf', 'platform')" + } + }, + "isRLSEnabled": false + }, + "public.email_domains": { + "name": "email_domains", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "zone_id": { + "name": "zone_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mode": { + "name": "mode", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "credential_kind": { + "name": "credential_kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token_ciphertext": { + "name": "token_ciphertext", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "token_iv": { + "name": "token_iv", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "token_key_version": { + "name": "token_key_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "managed_by": { + "name": "managed_by", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'owner'" + }, + "relay_ciphertext": { + "name": "relay_ciphertext", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "relay_iv": { + "name": "relay_iv", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "relay_key_version": { + "name": "relay_key_version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "relay_worker_name": { + "name": "relay_worker_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "relay_version": { + "name": "relay_version", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "token_id": { + "name": "token_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "token_expires_on": { + "name": "token_expires_on", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "attempt_id": { + "name": "attempt_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "setup_state": { + "name": "setup_state", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_email_domains_project_id": { + "name": "idx_email_domains_project_id", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "email_domains_zone_uq": { + "name": "email_domains_zone_uq", + "columns": [ + { + "expression": "zone_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"email_domains\".\"zone_id\" IS NOT NULL AND \"email_domains\".\"status\" IN ('active', 'pending')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "email_domains_relay_uq": { + "name": "email_domains_relay_uq", + "columns": [ + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "relay_worker_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"email_domains\".\"relay_worker_name\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "email_domains_project_id_projects_id_fk": { + "name": "email_domains_project_id_projects_id_fk", + "tableFrom": "email_domains", + "tableTo": "projects", + "columnsFrom": ["project_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "email_domains_domain_uq": { + "name": "email_domains_domain_uq", + "nullsNotDistinct": false, + "columns": ["domain"] + } + }, + "policies": {}, + "checkConstraints": { + "email_domains_mode_check": { + "name": "email_domains_mode_check", + "value": "\"email_domains\".\"mode\" IN ('apex', 'subdomain')" + }, + "email_domains_status_check": { + "name": "email_domains_status_check", + "value": "\"email_domains\".\"status\" IN ('pending', 'active', 'token_revoked', 'token_expired', 'failed')" + }, + "email_domains_credential_kind_check": { + "name": "email_domains_credential_kind_check", + "value": "\"email_domains\".\"credential_kind\" IN ('oauth_grant', 'api_token', 'platform')" + }, + "email_domains_platform_token_check": { + "name": "email_domains_platform_token_check", + "value": "(\"email_domains\".\"credential_kind\" = 'platform' AND \"email_domains\".\"token_ciphertext\" IS NULL AND \"email_domains\".\"token_iv\" IS NULL AND \"email_domains\".\"token_key_version\" IS NULL) OR (\"email_domains\".\"credential_kind\" <> 'platform' AND \"email_domains\".\"token_ciphertext\" IS NOT NULL AND \"email_domains\".\"token_iv\" IS NOT NULL AND \"email_domains\".\"token_key_version\" IS NOT NULL)" + }, + "email_domains_managed_by_check": { + "name": "email_domains_managed_by_check", + "value": "\"email_domains\".\"managed_by\" IN ('owner', 'admin')" + } + }, + "isRLSEnabled": false + }, + "public.github_connections": { + "name": "github_connections", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "installation_id": { + "name": "installation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "repo_full_name": { + "name": "repo_full_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "repository_id": { + "name": "repository_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "authorized_by_user_id": { + "name": "authorized_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "branch": { + "name": "branch", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'main'" + }, + "build_executor": { + "name": "build_executor", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'container'" + }, + "workflow_path": { + "name": "workflow_path", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'.github/workflows/void-deploy.yml'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_github_connections_installation_id": { + "name": "idx_github_connections_installation_id", + "columns": [ + { + "expression": "installation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_github_connections_repo_full_name": { + "name": "idx_github_connections_repo_full_name", + "columns": [ + { + "expression": "repo_full_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_github_connections_installation_repo_id": { + "name": "idx_github_connections_installation_repo_id", + "columns": [ + { + "expression": "installation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "repository_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "github_connections_project_id_projects_id_fk": { + "name": "github_connections_project_id_projects_id_fk", + "tableFrom": "github_connections", + "tableTo": "projects", + "columnsFrom": ["project_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "github_connections_authorized_by_user_id_users_id_fk": { + "name": "github_connections_authorized_by_user_id_users_id_fk", + "tableFrom": "github_connections", + "tableTo": "users", + "columnsFrom": ["authorized_by_user_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "github_connections_project_id_unique": { + "name": "github_connections_project_id_unique", + "nullsNotDistinct": false, + "columns": ["project_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.github_installation_members": { + "name": "github_installation_members", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "installation_id": { + "name": "installation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "github_account_id": { + "name": "github_account_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "verified_at": { + "name": "verified_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_github_installation_members_install_user": { + "name": "idx_github_installation_members_install_user", + "columns": [ + { + "expression": "installation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_github_installation_members_user_id": { + "name": "idx_github_installation_members_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "github_installation_members_user_id_users_id_fk": { + "name": "github_installation_members_user_id_users_id_fk", + "tableFrom": "github_installation_members", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.github_installation_revocations": { + "name": "github_installation_revocations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "installation_id": { + "name": "installation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "github_account_id": { + "name": "github_account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_github_installation_revocations_install_account": { + "name": "idx_github_installation_revocations_install_account", + "columns": [ + { + "expression": "installation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "github_account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.github_installations": { + "name": "github_installations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "installation_id": { + "name": "installation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "account_login": { + "name": "account_login", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "account_type": { + "name": "account_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "repositories": { + "name": "repositories", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "repository_selection": { + "name": "repository_selection", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'selected'" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_github_installations_user_id": { + "name": "idx_github_installations_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_github_installations_account_id": { + "name": "idx_github_installations_account_id", + "columns": [ + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "github_installations_user_id_users_id_fk": { + "name": "github_installations_user_id_users_id_fk", + "tableFrom": "github_installations", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "github_installations_installation_id_unique": { + "name": "github_installations_installation_id_unique", + "nullsNotDistinct": false, + "columns": ["installation_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.github_webhook_redeliveries": { + "name": "github_webhook_redeliveries", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "guid": { + "name": "guid", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "delivery_id": { + "name": "delivery_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "event": { + "name": "event", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status_code": { + "name": "status_code", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "redelivered_at": { + "name": "redelivered_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_github_webhook_redeliveries_guid": { + "name": "idx_github_webhook_redeliveries_guid", + "columns": [ + { + "expression": "guid", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.invitations": { + "name": "invitations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "email_delivery_status": { + "name": "email_delivery_status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "invited_by": { + "name": "invited_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "accepted_by": { + "name": "accepted_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "revoked_by": { + "name": "revoked_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "accepted_at": { + "name": "accepted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_invitations_email": { + "name": "idx_invitations_email", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_invitations_status": { + "name": "idx_invitations_status", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "invitations_email_delivery_status_check": { + "name": "invitations_email_delivery_status_check", + "value": "\"invitations\".\"email_delivery_status\" IS NULL OR \"invitations\".\"email_delivery_status\" IN ('sent', 'failed', 'unavailable')" + } + }, + "isRLSEnabled": false + }, + "public.migration_renames": { + "name": "migration_renames", + "schema": "", + "columns": { + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "from_name": { + "name": "from_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "to_name": { + "name": "to_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_migration_renames_project_id": { + "name": "idx_migration_renames_project_id", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "migration_renames_project_id_projects_id_fk": { + "name": "migration_renames_project_id_projects_id_fk", + "tableFrom": "migration_renames", + "tableTo": "projects", + "columnsFrom": ["project_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "migration_renames_project_id_from_name_to_name_pk": { + "name": "migration_renames_project_id_from_name_to_name_pk", + "columns": ["project_id", "from_name", "to_name"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.oauth_accounts": { + "name": "oauth_accounts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_oauth_accounts_provider_id": { + "name": "idx_oauth_accounts_provider_id", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_oauth_accounts_user_id": { + "name": "idx_oauth_accounts_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "oauth_accounts_user_id_users_id_fk": { + "name": "oauth_accounts_user_id_users_id_fk", + "tableFrom": "oauth_accounts", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.oauth_clients": { + "name": "oauth_clients", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "operator": { + "name": "operator", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "redirect_uris": { + "name": "redirect_uris", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "scopes": { + "name": "scopes", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "secret_hash": { + "name": "secret_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.oauth_grants": { + "name": "oauth_grants", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session": { + "name": "session", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "scopes": { + "name": "scopes", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[\"profile\"]'::jsonb" + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "oauth_grants_token": { + "name": "oauth_grants_token", + "columns": [ + { + "expression": "token_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "oauth_grants_user": { + "name": "oauth_grants_user", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "oauth_grants_expiry": { + "name": "oauth_grants_expiry", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "oauth_grants_client_id_oauth_clients_id_fk": { + "name": "oauth_grants_client_id_oauth_clients_id_fk", + "tableFrom": "oauth_grants", + "tableTo": "oauth_clients", + "columnsFrom": ["client_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "oauth_grants_user_id_users_id_fk": { + "name": "oauth_grants_user_id_users_id_fk", + "tableFrom": "oauth_grants", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.oauth_nonces": { + "name": "oauth_nonces", + "schema": "", + "columns": { + "nonce": { + "name": "nonce", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.oauth_requests": { + "name": "oauth_requests", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "browser_hash": { + "name": "browser_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "code_hash": { + "name": "code_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "code_expires_at": { + "name": "code_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "approved_at": { + "name": "approved_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "consumed_at": { + "name": "consumed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "oauth_requests_code": { + "name": "oauth_requests_code", + "columns": [ + { + "expression": "code_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "oauth_requests_expiry": { + "name": "oauth_requests_expiry", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "oauth_requests_client_id_oauth_clients_id_fk": { + "name": "oauth_requests_client_id_oauth_clients_id_fk", + "tableFrom": "oauth_requests", + "tableTo": "oauth_clients", + "columnsFrom": ["client_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "oauth_requests_user_id_users_id_fk": { + "name": "oauth_requests_user_id_users_id_fk", + "tableFrom": "oauth_requests", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.operator_events": { + "name": "operator_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "login": { + "name": "login", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target": { + "name": "target", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "http_status": { + "name": "http_status", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_operator_events_created": { + "name": "idx_operator_events_created", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.operator_sessions": { + "name": "operator_sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "human_session_id": { + "name": "human_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_operator_sessions_expiry": { + "name": "idx_operator_sessions_expiry", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "operator_sessions_user_id_users_id_fk": { + "name": "operator_sessions_user_id_users_id_fk", + "tableFrom": "operator_sessions", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "operator_sessions_human_session_id_platform_human_sessions_id_fk": { + "name": "operator_sessions_human_session_id_platform_human_sessions_id_fk", + "tableFrom": "operator_sessions", + "tableTo": "platform_human_sessions", + "columnsFrom": ["human_session_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.platform_auth_attempts": { + "name": "platform_auth_attempts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "revision": { + "name": "revision", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "generation": { + "name": "generation", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "purpose": { + "name": "purpose", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "browser_hash": { + "name": "browser_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "context": { + "name": "context", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "platform_auth_attempts_expiry": { + "name": "platform_auth_attempts_expiry", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "platform_auth_attempts_connection_id_platform_login_connections_id_fk": { + "name": "platform_auth_attempts_connection_id_platform_login_connections_id_fk", + "tableFrom": "platform_auth_attempts", + "tableTo": "platform_login_connections", + "columnsFrom": ["connection_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.platform_auth_bootstrap": { + "name": "platform_auth_bootstrap", + "schema": "", + "columns": { + "singleton": { + "name": "singleton", + "type": "boolean", + "primaryKey": true, + "notNull": true, + "default": true + }, + "installation_id": { + "name": "installation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "credential_hash": { + "name": "credential_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "request_hash": { + "name": "request_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "claim_hash": { + "name": "claim_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pending_identity": { + "name": "pending_identity", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "consumed_at": { + "name": "consumed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "platform_auth_bootstrap_connection_id_platform_login_connections_id_fk": { + "name": "platform_auth_bootstrap_connection_id_platform_login_connections_id_fk", + "tableFrom": "platform_auth_bootstrap", + "tableTo": "platform_login_connections", + "columnsFrom": ["connection_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "platform_auth_bootstrap_singleton": { + "name": "platform_auth_bootstrap_singleton", + "value": "\"platform_auth_bootstrap\".\"singleton\" = true" + } + }, + "isRLSEnabled": false + }, + "public.platform_auth_settings": { + "name": "platform_auth_settings", + "schema": "", + "columns": { + "singleton": { + "name": "singleton", + "type": "boolean", + "primaryKey": true, + "notNull": true, + "default": true + }, + "revision": { + "name": "revision", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "admission": { + "name": "admission", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "protection": { + "name": "protection", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "session_seconds": { + "name": "session_seconds", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 2592000 + }, + "sessions_required": { + "name": "sessions_required", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "platform_auth_settings_singleton": { + "name": "platform_auth_settings_singleton", + "value": "\"platform_auth_settings\".\"singleton\" = true" + }, + "platform_auth_settings_revision": { + "name": "platform_auth_settings_revision", + "value": "\"platform_auth_settings\".\"revision\" > 0" + }, + "platform_auth_settings_session_seconds": { + "name": "platform_auth_settings_session_seconds", + "value": "\"platform_auth_settings\".\"session_seconds\" BETWEEN 300 AND 2592000" + } + }, + "isRLSEnabled": false + }, + "public.platform_company_identities": { + "name": "platform_company_identities", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "issuer": { + "name": "issuer", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "subject": { + "name": "subject", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "platform_company_identity_user": { + "name": "platform_company_identity_user", + "columns": [ + { + "expression": "issuer", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "platform_company_identities_user_id_users_id_fk": { + "name": "platform_company_identities_user_id_users_id_fk", + "tableFrom": "platform_company_identities", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "platform_company_identities_issuer_subject_pk": { + "name": "platform_company_identities_issuer_subject_pk", + "columns": ["issuer", "subject"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.platform_events": { + "name": "platform_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "batch_id": { + "name": "batch_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "worker_name": { + "name": "worker_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment": { + "name": "environment", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version_id": { + "name": "version_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "script_name": { + "name": "script_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "admin_user_id": { + "name": "admin_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "admin_login": { + "name": "admin_login", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_platform_events_batch_id": { + "name": "idx_platform_events_batch_id", + "columns": [ + { + "expression": "batch_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_platform_events_env_created": { + "name": "idx_platform_events_env_created", + "columns": [ + { + "expression": "environment", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.platform_human_sessions": { + "name": "platform_human_sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "generation": { + "name": "generation", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "audience": { + "name": "audience", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_issuer": { + "name": "access_issuer", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_subject": { + "name": "access_subject", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "platform_human_sessions_connection": { + "name": "platform_human_sessions_connection", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "platform_human_sessions_user": { + "name": "platform_human_sessions_user", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "platform_human_sessions_expiry": { + "name": "platform_human_sessions_expiry", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "platform_human_sessions_user_id_users_id_fk": { + "name": "platform_human_sessions_user_id_users_id_fk", + "tableFrom": "platform_human_sessions", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "platform_human_sessions_connection_id_platform_login_connections_id_fk": { + "name": "platform_human_sessions_connection_id_platform_login_connections_id_fk", + "tableFrom": "platform_human_sessions", + "tableTo": "platform_login_connections", + "columnsFrom": ["connection_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "platform_human_sessions_audience": { + "name": "platform_human_sessions_audience", + "value": "\"platform_human_sessions\".\"audience\" IN ('api', 'admin')" + }, + "platform_human_sessions_access_identity": { + "name": "platform_human_sessions_access_identity", + "value": "(\"platform_human_sessions\".\"access_issuer\" IS NULL) = (\"platform_human_sessions\".\"access_subject\" IS NULL)" + } + }, + "isRLSEnabled": false + }, + "public.platform_login_connections": { + "name": "platform_login_connections", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "active_revision": { + "name": "active_revision", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "revision": { + "name": "revision", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "generation": { + "name": "generation", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "platform_login_connections_kind": { + "name": "platform_login_connections_kind", + "value": "\"platform_login_connections\".\"kind\" IN ('github', 'google', 'oidc', 'cloudflare-access')" + }, + "platform_login_connections_revision": { + "name": "platform_login_connections_revision", + "value": "\"platform_login_connections\".\"revision\" > 0 AND \"platform_login_connections\".\"generation\" > 0" + }, + "platform_login_connections_active_revision": { + "name": "platform_login_connections_active_revision", + "value": "\"platform_login_connections\".\"active_revision\" IS NULL OR \"platform_login_connections\".\"active_revision\" BETWEEN 1 AND \"platform_login_connections\".\"revision\"" + }, + "platform_login_connections_enabled": { + "name": "platform_login_connections_enabled", + "value": "NOT \"platform_login_connections\".\"enabled\" OR \"platform_login_connections\".\"active_revision\" IS NOT NULL" + } + }, + "isRLSEnabled": false + }, + "public.platform_login_identities": { + "name": "platform_login_identities", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "issuer": { + "name": "issuer", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "subject": { + "name": "subject", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_authenticated_at": { + "name": "last_authenticated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "platform_login_identity_subject": { + "name": "platform_login_identity_subject", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issuer", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "subject", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "platform_login_identity_user": { + "name": "platform_login_identity_user", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "platform_login_identities_user_id_users_id_fk": { + "name": "platform_login_identities_user_id_users_id_fk", + "tableFrom": "platform_login_identities", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "platform_login_identities_connection_id_platform_login_connections_id_fk": { + "name": "platform_login_identities_connection_id_platform_login_connections_id_fk", + "tableFrom": "platform_login_identities", + "tableTo": "platform_login_connections", + "columnsFrom": ["connection_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.platform_login_revisions": { + "name": "platform_login_revisions", + "schema": "", + "columns": { + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "revision": { + "name": "revision", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "configuration": { + "name": "configuration", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "secret": { + "name": "secret", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "platform_login_revisions_connection_id_platform_login_connections_id_fk": { + "name": "platform_login_revisions_connection_id_platform_login_connections_id_fk", + "tableFrom": "platform_login_revisions", + "tableTo": "platform_login_connections", + "columnsFrom": ["connection_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "platform_login_revisions_connection_id_revision_pk": { + "name": "platform_login_revisions_connection_id_revision_pk", + "columns": ["connection_id", "revision"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.platform_login_tests": { + "name": "platform_login_tests", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "revision": { + "name": "revision", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "issuer": { + "name": "issuer", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "subject": { + "name": "subject", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "login": { + "name": "login", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "platform_login_tests_expiry": { + "name": "platform_login_tests_expiry", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "platform_login_tests_user_id_users_id_fk": { + "name": "platform_login_tests_user_id_users_id_fk", + "tableFrom": "platform_login_tests", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "platform_login_tests_connection_id_platform_login_connections_id_fk": { + "name": "platform_login_tests_connection_id_platform_login_connections_id_fk", + "tableFrom": "platform_login_tests", + "tableTo": "platform_login_connections", + "columnsFrom": ["connection_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.platform_settings": { + "name": "platform_settings", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "value": { + "name": "value", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.preview_tickets": { + "name": "preview_tickets", + "schema": "", + "columns": { + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "grant_id": { + "name": "grant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "preview_tickets_project_id_projects_id_fk": { + "name": "preview_tickets_project_id_projects_id_fk", + "tableFrom": "preview_tickets", + "tableTo": "projects", + "columnsFrom": ["project_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "preview_tickets_grant_id_oauth_grants_id_fk": { + "name": "preview_tickets_grant_id_oauth_grants_id_fk", + "tableFrom": "preview_tickets", + "tableTo": "oauth_grants", + "columnsFrom": ["grant_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.project_access_events": { + "name": "project_access_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actor_user_id": { + "name": "actor_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "subject_user_id": { + "name": "subject_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "invitation_email_snapshot": { + "name": "invitation_email_snapshot", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "previous_role": { + "name": "previous_role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "new_role": { + "name": "new_role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_project_access_events_project_created": { + "name": "idx_project_access_events_project_created", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_project_access_events_subject_created": { + "name": "idx_project_access_events_subject_created", + "columns": [ + { + "expression": "subject_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "project_access_events_project_id_projects_id_fk": { + "name": "project_access_events_project_id_projects_id_fk", + "tableFrom": "project_access_events", + "tableTo": "projects", + "columnsFrom": ["project_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "project_access_events_previous_role_check": { + "name": "project_access_events_previous_role_check", + "value": "\"project_access_events\".\"previous_role\" IS NULL OR \"project_access_events\".\"previous_role\" IN ('reader', 'collaborator', 'admin')" + }, + "project_access_events_new_role_check": { + "name": "project_access_events_new_role_check", + "value": "\"project_access_events\".\"new_role\" IS NULL OR \"project_access_events\".\"new_role\" IN ('reader', 'collaborator', 'admin')" + }, + "project_access_events_action_check": { + "name": "project_access_events_action_check", + "value": "\"project_access_events\".\"action\" IN ('invited', 'invitation_resent', 'member_added', 'accepted', 'declined', 'invitation_revoked', 'role_changed', 'member_removed', 'left')" + } + }, + "isRLSEnabled": false + }, + "public.project_cleanup_jobs": { + "name": "project_cleanup_jobs", + "schema": "", + "columns": { + "target": { + "name": "target", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "next_attempt_at": { + "name": "next_attempt_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "project_cleanup_jobs_due": { + "name": "project_cleanup_jobs_due", + "columns": [ + { + "expression": "next_attempt_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.project_database_credentials": { + "name": "project_database_credentials", + "schema": "", + "columns": { + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "ciphertext": { + "name": "ciphertext", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "iv": { + "name": "iv", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key_version": { + "name": "key_version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "project_database_credentials_project_id_projects_id_fk": { + "name": "project_database_credentials_project_id_projects_id_fk", + "tableFrom": "project_database_credentials", + "tableTo": "projects", + "columnsFrom": ["project_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.project_deploy_credentials": { + "name": "project_deploy_credentials", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "issued_by": { + "name": "issued_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "project_deploy_credentials_project": { + "name": "project_deploy_credentials_project", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "project_deploy_credentials_expiry": { + "name": "project_deploy_credentials_expiry", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "project_deploy_credentials_project_id_projects_id_fk": { + "name": "project_deploy_credentials_project_id_projects_id_fk", + "tableFrom": "project_deploy_credentials", + "tableTo": "projects", + "columnsFrom": ["project_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "project_deploy_credentials_issued_by_users_id_fk": { + "name": "project_deploy_credentials_issued_by_users_id_fk", + "tableFrom": "project_deploy_credentials", + "tableTo": "users", + "columnsFrom": ["issued_by"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "project_deploy_credentials_name": { + "name": "project_deploy_credentials_name", + "value": "length(\"project_deploy_credentials\".\"name\") BETWEEN 1 AND 80" + } + }, + "isRLSEnabled": false + }, + "public.project_invitations": { + "name": "project_invitations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_user_id": { + "name": "target_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_email_snapshot": { + "name": "target_email_snapshot", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "invited_by_user_id": { + "name": "invited_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "accepted_by_user_id": { + "name": "accepted_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "accepted_at": { + "name": "accepted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "revoked_by_user_id": { + "name": "revoked_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_project_invitations_project_status": { + "name": "idx_project_invitations_project_status", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_project_invitations_target_status": { + "name": "idx_project_invitations_target_status", + "columns": [ + { + "expression": "target_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_project_invitations_pending_target": { + "name": "idx_project_invitations_pending_target", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"project_invitations\".\"status\" = 'pending'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "project_invitations_project_id_projects_id_fk": { + "name": "project_invitations_project_id_projects_id_fk", + "tableFrom": "project_invitations", + "tableTo": "projects", + "columnsFrom": ["project_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "project_invitations_target_user_id_users_id_fk": { + "name": "project_invitations_target_user_id_users_id_fk", + "tableFrom": "project_invitations", + "tableTo": "users", + "columnsFrom": ["target_user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "project_invitations_role_check": { + "name": "project_invitations_role_check", + "value": "\"project_invitations\".\"role\" IN ('reader', 'collaborator', 'admin')" + }, + "project_invitations_status_check": { + "name": "project_invitations_status_check", + "value": "\"project_invitations\".\"status\" IN ('pending', 'accepted', 'declined', 'revoked', 'expired')" + } + }, + "isRLSEnabled": false + }, + "public.project_members": { + "name": "project_members", + "schema": "", + "columns": { + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "added_by_user_id": { + "name": "added_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_project_members_user_project": { + "name": "idx_project_members_user_project", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "project_members_project_id_projects_id_fk": { + "name": "project_members_project_id_projects_id_fk", + "tableFrom": "project_members", + "tableTo": "projects", + "columnsFrom": ["project_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "project_members_user_id_users_id_fk": { + "name": "project_members_user_id_users_id_fk", + "tableFrom": "project_members", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "project_members_project_id_user_id_pk": { + "name": "project_members_project_id_user_id_pk", + "columns": ["project_id", "user_id"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "project_members_role_check": { + "name": "project_members_role_check", + "value": "\"project_members\".\"role\" IN ('reader', 'collaborator', 'admin')" + } + }, + "isRLSEnabled": false + }, + "public.project_migrations": { + "name": "project_migrations", + "schema": "", + "columns": { + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "applied_at": { + "name": "applied_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "source_deployment_id": { + "name": "source_deployment_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_project_migrations_project_id": { + "name": "idx_project_migrations_project_id", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "applied_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "project_migrations_project_id_projects_id_fk": { + "name": "project_migrations_project_id_projects_id_fk", + "tableFrom": "project_migrations", + "tableTo": "projects", + "columnsFrom": ["project_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "project_migrations_source_deployment_id_deployments_id_fk": { + "name": "project_migrations_source_deployment_id_deployments_id_fk", + "tableFrom": "project_migrations", + "tableTo": "deployments", + "columnsFrom": ["source_deployment_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "project_migrations_project_id_name_pk": { + "name": "project_migrations_project_id_name_pk", + "columns": ["project_id", "name"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.project_ownership_periods": { + "name": "project_ownership_periods", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "starts_at": { + "name": "starts_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ends_at": { + "name": "ends_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "changed_by_user_id": { + "name": "changed_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_project_ownership_periods_owner_time": { + "name": "idx_project_ownership_periods_owner_time", + "columns": [ + { + "expression": "owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "starts_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "ends_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_project_ownership_periods_project_time": { + "name": "idx_project_ownership_periods_project_time", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "starts_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_project_ownership_periods_open": { + "name": "idx_project_ownership_periods_open", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"project_ownership_periods\".\"ends_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "project_ownership_periods_project_id_projects_id_fk": { + "name": "project_ownership_periods_project_id_projects_id_fk", + "tableFrom": "project_ownership_periods", + "tableTo": "projects", + "columnsFrom": ["project_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "project_ownership_periods_time_check": { + "name": "project_ownership_periods_time_check", + "value": "\"project_ownership_periods\".\"ends_at\" IS NULL OR \"project_ownership_periods\".\"ends_at\" >= \"project_ownership_periods\".\"starts_at\"" + } + }, + "isRLSEnabled": false + }, + "public.project_secrets": { + "name": "project_secrets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ciphertext": { + "name": "ciphertext", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "iv": { + "name": "iv", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key_version": { + "name": "key_version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "managed_reason": { + "name": "managed_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_project_secrets_project_id": { + "name": "idx_project_secrets_project_id", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "project_secrets_project_id_projects_id_fk": { + "name": "project_secrets_project_id_projects_id_fk", + "tableFrom": "project_secrets", + "tableTo": "projects", + "columnsFrom": ["project_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "project_secrets_project_id_name_unique": { + "name": "project_secrets_project_id_name_unique", + "nullsNotDistinct": false, + "columns": ["project_id", "name"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.project_zero_trust": { + "name": "project_zero_trust", + "schema": "", + "columns": { + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "desired_protected": { + "name": "desired_protected", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "effective_protected": { + "name": "effective_protected", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'ready'" + }, + "bypass_application_id": { + "name": "bypass_application_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bypass_application_name": { + "name": "bypass_application_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "protected_application_id": { + "name": "protected_application_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "protected_application_name": { + "name": "protected_application_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "protected_application_audience": { + "name": "protected_application_audience", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "routing_cursor": { + "name": "routing_cursor", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reconciled_operation": { + "name": "reconciled_operation", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_project_zero_trust_status": { + "name": "idx_project_zero_trust_status", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "project_zero_trust_project_id_projects_id_fk": { + "name": "project_zero_trust_project_id_projects_id_fk", + "tableFrom": "project_zero_trust", + "tableTo": "projects", + "columnsFrom": ["project_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "project_zero_trust_status_check": { + "name": "project_zero_trust_status_check", + "value": "\"project_zero_trust\".\"status\" IN ('ready', 'reconciling', 'error')" + }, + "project_zero_trust_ready_check": { + "name": "project_zero_trust_ready_check", + "value": "\"project_zero_trust\".\"status\" <> 'ready' OR \"project_zero_trust\".\"desired_protected\" = \"project_zero_trust\".\"effective_protected\"" + } + }, + "isRLSEnabled": false + }, + "public.projects": { + "name": "projects", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "preview_client_id": { + "name": "preview_client_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "preview_workspace_id": { + "name": "preview_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "preview_expires_at": { + "name": "preview_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "mode": { + "name": "mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'void'" + }, + "active_deployment_id": { + "name": "active_deployment_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "websocket_version": { + "name": "websocket_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "proxy_token_gen": { + "name": "proxy_token_gen", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_projects_slug": { + "name": "idx_projects_slug", + "columns": [ + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_projects_user_id": { + "name": "idx_projects_user_id", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "projects_preview_workspace": { + "name": "projects_preview_workspace", + "columns": [ + { + "expression": "preview_client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "preview_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "projects_user_id_users_id_fk": { + "name": "projects_user_id_users_id_fk", + "tableFrom": "projects", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "projects_preview_client_id_oauth_clients_id_fk": { + "name": "projects_preview_client_id_oauth_clients_id_fk", + "tableFrom": "projects", + "tableTo": "oauth_clients", + "columnsFrom": ["preview_client_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "projects_user_id_slug_unique": { + "name": "projects_user_id_slug_unique", + "nullsNotDistinct": false, + "columns": ["user_id", "slug"] + } + }, + "policies": {}, + "checkConstraints": { + "projects_preview_identity": { + "name": "projects_preview_identity", + "value": "(\"projects\".\"preview_client_id\" IS NULL AND \"projects\".\"preview_workspace_id\" IS NULL AND \"projects\".\"preview_expires_at\" IS NULL) OR (\"projects\".\"preview_client_id\" IS NOT NULL AND \"projects\".\"preview_workspace_id\" IS NOT NULL AND \"projects\".\"preview_expires_at\" IS NOT NULL AND \"projects\".\"slug\" LIKE 'preview-%')" + } + }, + "isRLSEnabled": false + }, + "public.resources": { + "name": "resources", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "cf_id": { + "name": "cf_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "binding_name": { + "name": "binding_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "resources_project_id_projects_id_fk": { + "name": "resources_project_id_projects_id_fk", + "tableFrom": "resources", + "tableTo": "projects", + "columnsFrom": ["project_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "resources_project_id_type_binding_name_unique": { + "name": "resources_project_id_type_binding_name_unique", + "nullsNotDistinct": false, + "columns": ["project_id", "type", "binding_name"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sandbox_drain_checkpoints": { + "name": "sandbox_drain_checkpoints", + "schema": "", + "columns": { + "singleton": { + "name": "singleton", + "type": "boolean", + "primaryKey": true, + "notNull": true + }, + "admission_protocol": { + "name": "admission_protocol", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "generation": { + "name": "generation", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "phase": { + "name": "phase", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "cursor": { + "name": "cursor", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "lease_owner": { + "name": "lease_owner", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "lease_expires_at": { + "name": "lease_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'github'" + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "login": { + "name": "login", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "plan": { + "name": "plan", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'free'" + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'user'" + }, + "billing_cycle_start": { + "name": "billing_cycle_start", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "suspended_reason": { + "name": "suspended_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_users_provider_id": { + "name": "idx_users_provider_id", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_users_email": { + "name": "idx_users_email", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.email_cache_outbox": { + "name": "email_cache_outbox", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "installation_id": { + "name": "installation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_kind": { + "name": "resource_kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "published_at": { + "name": "published_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "email_cache_outbox_version_uq": { + "name": "email_cache_outbox_version_uq", + "columns": [ + { + "expression": "resource_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "version", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "email_cache_outbox_pending_idx": { + "name": "email_cache_outbox_pending_idx", + "columns": [ + { + "expression": "published_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.email_credentials": { + "name": "email_credentials", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "installation_id": { + "name": "installation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "managed_by": { + "name": "managed_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "manager_user_id": { + "name": "manager_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "zone_id": { + "name": "zone_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ciphertext": { + "name": "ciphertext", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "iv": { + "name": "iv", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key_version": { + "name": "key_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "token_id": { + "name": "token_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_on": { + "name": "expires_on", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "generation": { + "name": "generation", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'healthy'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "email_credentials_scope_idx": { + "name": "email_credentials_scope_idx", + "columns": [ + { + "expression": "installation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "zone_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "email_credentials_material_check": { + "name": "email_credentials_material_check", + "value": "(\"email_credentials\".\"kind\" = 'platform' AND \"email_credentials\".\"ciphertext\" IS NULL AND \"email_credentials\".\"iv\" IS NULL AND \"email_credentials\".\"key_version\" IS NULL) OR (\"email_credentials\".\"kind\" = 'api_token' AND \"email_credentials\".\"ciphertext\" IS NOT NULL AND \"email_credentials\".\"iv\" IS NOT NULL AND \"email_credentials\".\"key_version\" IS NOT NULL)" + } + }, + "isRLSEnabled": false + }, + "public.email_domain_routes": { + "name": "email_domain_routes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "addressing": { + "name": "addressing", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "managed_by": { + "name": "managed_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "assigned_by_user_id": { + "name": "assigned_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "desired_state": { + "name": "desired_state", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "inbound_mode": { + "name": "inbound_mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'managed'" + }, + "inbound_status": { + "name": "inbound_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "inbound_reason": { + "name": "inbound_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "inbound_observed_at": { + "name": "inbound_observed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "outbound_status": { + "name": "outbound_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "outbound_reason": { + "name": "outbound_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "outbound_observed_at": { + "name": "outbound_observed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "desired_generation": { + "name": "desired_generation", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "email_domain_routes_domain_uq": { + "name": "email_domain_routes_domain_uq", + "columns": [ + { + "expression": "domain", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "email_domain_routes_connection_idx": { + "name": "email_domain_routes_connection_idx", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "email_domain_routes_project_idx": { + "name": "email_domain_routes_project_idx", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "email_domain_routes_connection_id_email_zone_connections_id_fk": { + "name": "email_domain_routes_connection_id_email_zone_connections_id_fk", + "tableFrom": "email_domain_routes", + "tableTo": "email_zone_connections", + "columnsFrom": ["connection_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "email_domain_routes_assignment_check": { + "name": "email_domain_routes_assignment_check", + "value": "(\"email_domain_routes\".\"addressing\" = 'custom' AND \"email_domain_routes\".\"project_id\" IS NOT NULL) OR (\"email_domain_routes\".\"addressing\" = 'shared' AND \"email_domain_routes\".\"project_id\" IS NULL)" + }, + "email_domain_routes_inbound_mode_check": { + "name": "email_domain_routes_inbound_mode_check", + "value": "\"email_domain_routes\".\"inbound_mode\" = 'managed' OR (\"email_domain_routes\".\"inbound_mode\" = 'none' AND \"email_domain_routes\".\"addressing\" = 'custom')" + } + }, + "isRLSEnabled": false + }, + "public.email_operation_steps": { + "name": "email_operation_steps", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "operation_id": { + "name": "operation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ordinal": { + "name": "ordinal", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_identity": { + "name": "resource_identity", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "request_digest": { + "name": "request_digest", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "attempt": { + "name": "attempt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "outcome": { + "name": "outcome", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "failure_code": { + "name": "failure_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "failure_detail": { + "name": "failure_detail", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "finished_at": { + "name": "finished_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "email_operation_steps_order_uq": { + "name": "email_operation_steps_order_uq", + "columns": [ + { + "expression": "operation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "ordinal", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "email_operation_steps_operation_id_email_operations_id_fk": { + "name": "email_operation_steps_operation_id_email_operations_id_fk", + "tableFrom": "email_operation_steps", + "tableTo": "email_operations", + "columnsFrom": ["operation_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.email_operations": { + "name": "email_operations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "installation_id": { + "name": "installation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "route_id": { + "name": "route_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "route_generation": { + "name": "route_generation", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "actor_kind": { + "name": "actor_kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actor_user_id": { + "name": "actor_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_scope": { + "name": "actor_scope", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "request_digest": { + "name": "request_digest", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "desired_generation": { + "name": "desired_generation", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "current_step": { + "name": "current_step", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "failure_code": { + "name": "failure_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "failure_detail": { + "name": "failure_detail", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "email_operations_key_uq": { + "name": "email_operations_key_uq", + "columns": [ + { + "expression": "installation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "actor_scope", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "email_operations_work_idx": { + "name": "email_operations_work_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "email_operations_connection_idx": { + "name": "email_operations_connection_idx", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "email_operations_connection_id_email_zone_connections_id_fk": { + "name": "email_operations_connection_id_email_zone_connections_id_fk", + "tableFrom": "email_operations", + "tableTo": "email_zone_connections", + "columnsFrom": ["connection_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "email_operations_route_id_email_domain_routes_id_fk": { + "name": "email_operations_route_id_email_domain_routes_id_fk", + "tableFrom": "email_operations", + "tableTo": "email_domain_routes", + "columnsFrom": ["route_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.email_shared_recipients": { + "name": "email_shared_recipients", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "installation_id": { + "name": "installation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "route_id": { + "name": "route_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "recipient": { + "name": "recipient", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "rule_name": { + "name": "rule_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "rule_id": { + "name": "rule_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "create_started_at": { + "name": "create_started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "removing": { + "name": "removing", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "attempt": { + "name": "attempt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "lease_started_at": { + "name": "lease_started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "observed_at": { + "name": "observed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "email_shared_recipients_project_address_uq": { + "name": "email_shared_recipients_project_address_uq", + "columns": [ + { + "expression": "installation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "recipient", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "email_shared_recipients_rule_name_uq": { + "name": "email_shared_recipients_rule_name_uq", + "columns": [ + { + "expression": "rule_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "email_shared_recipients_reconcile_idx": { + "name": "email_shared_recipients_reconcile_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.email_zone_connections": { + "name": "email_zone_connections", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "installation_id": { + "name": "installation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "zone_id": { + "name": "zone_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "zone_name": { + "name": "zone_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "managed_by": { + "name": "managed_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "manager_user_id": { + "name": "manager_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ingress_worker_name": { + "name": "ingress_worker_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ingress_version": { + "name": "ingress_version", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "desired_generation": { + "name": "desired_generation", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "active_secret_generation": { + "name": "active_secret_generation", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "staged_secret_generation": { + "name": "staged_secret_generation", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "secret_ciphertext": { + "name": "secret_ciphertext", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "secret_iv": { + "name": "secret_iv", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "secret_key_version": { + "name": "secret_key_version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "staged_secret_ciphertext": { + "name": "staged_secret_ciphertext", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "staged_secret_iv": { + "name": "staged_secret_iv", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "staged_secret_key_version": { + "name": "staged_secret_key_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "previous_secret_hash": { + "name": "previous_secret_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "previous_secret_until": { + "name": "previous_secret_until", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "inbound_status": { + "name": "inbound_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "inbound_reason": { + "name": "inbound_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "inbound_observed_at": { + "name": "inbound_observed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "management_status": { + "name": "management_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'probe_failed'" + }, + "management_reason": { + "name": "management_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "management_observed_at": { + "name": "management_observed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "email_zone_connections_zone_uq": { + "name": "email_zone_connections_zone_uq", + "columns": [ + { + "expression": "installation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "zone_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "email_zone_connections_ingress_uq": { + "name": "email_zone_connections_ingress_uq", + "columns": [ + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "ingress_worker_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "email_zone_connections_manager_idx": { + "name": "email_zone_connections_manager_idx", + "columns": [ + { + "expression": "manager_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "email_zone_connections_manager_check": { + "name": "email_zone_connections_manager_check", + "value": "(\"email_zone_connections\".\"managed_by\" = 'owner' AND \"email_zone_connections\".\"manager_user_id\" IS NOT NULL) OR (\"email_zone_connections\".\"managed_by\" = 'admin')" + } + }, + "isRLSEnabled": false + }, + "public.email_action_recipients": { + "name": "email_action_recipients", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "action_id": { + "name": "action_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "recipient": { + "name": "recipient", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "error_code": { + "name": "error_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_reference": { + "name": "provider_reference", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reserved_at": { + "name": "reserved_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "attempt_started_at": { + "name": "attempt_started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_email_action_recipients_project_reserved": { + "name": "idx_email_action_recipients_project_reserved", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "reserved_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_email_action_recipients_expiring": { + "name": "idx_email_action_recipients_expiring", + "columns": [ + { + "expression": "reserved_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"email_action_recipients\".\"state\" = 'reserved'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "email_action_recipients_action_id_email_actions_id_fk": { + "name": "email_action_recipients_action_id_email_actions_id_fk", + "tableFrom": "email_action_recipients", + "tableTo": "email_actions", + "columnsFrom": ["action_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "email_action_recipients_action_recipient_uq": { + "name": "email_action_recipients_action_recipient_uq", + "nullsNotDistinct": false, + "columns": ["action_id", "recipient"] + } + }, + "policies": {}, + "checkConstraints": { + "email_action_recipients_state_check": { + "name": "email_action_recipients_state_check", + "value": "\"email_action_recipients\".\"state\" IN ('rejected', 'reserved', 'attempt_started', 'provider_accepted', 'failed', 'outcome_unknown', 'cancelled')" + } + }, + "isRLSEnabled": false + }, + "public.email_actions": { + "name": "email_actions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sender_domain": { + "name": "sender_domain", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "receipt_id": { + "name": "receipt_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload_digest": { + "name": "payload_digest", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "transport": { + "name": "transport", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "policy_version": { + "name": "policy_version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_email_actions_project_created": { + "name": "idx_email_actions_project_created", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "email_actions_receipt_id_email_receipts_id_fk": { + "name": "email_actions_receipt_id_email_receipts_id_fk", + "tableFrom": "email_actions", + "tableTo": "email_receipts", + "columnsFrom": ["receipt_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "email_actions_project_key_uq": { + "name": "email_actions_project_key_uq", + "nullsNotDistinct": false, + "columns": ["project_id", "idempotency_key"] + } + }, + "policies": {}, + "checkConstraints": { + "email_actions_kind_check": { + "name": "email_actions_kind_check", + "value": "\"email_actions\".\"kind\" IN ('direct', 'forward', 'reply')" + } + }, + "isRLSEnabled": false + }, + "public.email_monthly_usage": { + "name": "email_monthly_usage", + "schema": "", + "columns": { + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "month": { + "name": "month", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "recipient_attempts": { + "name": "recipient_attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "inbound_receipts": { + "name": "inbound_receipts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "email_monthly_usage_project_month_uq": { + "name": "email_monthly_usage_project_month_uq", + "nullsNotDistinct": false, + "columns": ["project_id", "month"] + } + }, + "policies": {}, + "checkConstraints": { + "email_monthly_usage_nonnegative_check": { + "name": "email_monthly_usage_nonnegative_check", + "value": "\"email_monthly_usage\".\"recipient_attempts\" >= 0" + }, + "email_monthly_usage_inbound_nonnegative_check": { + "name": "email_monthly_usage_inbound_nonnegative_check", + "value": "\"email_monthly_usage\".\"inbound_receipts\" >= 0" + } + }, + "isRLSEnabled": false + }, + "public.email_project_controls": { + "name": "email_project_controls", + "schema": "", + "columns": { + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "monthly_limit": { + "name": "monthly_limit", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 200 + }, + "burst_limit": { + "name": "burst_limit", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 10 + }, + "suspended_at": { + "name": "suspended_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + } + }, + "indexes": {}, + "foreignKeys": { + "email_project_controls_project_id_projects_id_fk": { + "name": "email_project_controls_project_id_projects_id_fk", + "tableFrom": "email_project_controls", + "tableTo": "projects", + "columnsFrom": ["project_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.email_receipts": { + "name": "email_receipts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'received'" + }, + "payload_digest": { + "name": "payload_digest", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "final_reject": { + "name": "final_reject", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idx_email_receipts_project_created": { + "name": "idx_email_receipts_project_created", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.email_unresolved_attempts": { + "name": "email_unresolved_attempts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sender_domain": { + "name": "sender_domain", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "idx_email_unresolved_attempts_project": { + "name": "idx_email_unresolved_attempts_project", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_email_unresolved_attempts_domain": { + "name": "idx_email_unresolved_attempts_domain", + "columns": [ + { + "expression": "sender_domain", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} diff --git a/platform/packages/api/drizzle/meta/_journal.json b/platform/packages/api/drizzle/meta/_journal.json index 18298de80..63d800f45 100644 --- a/platform/packages/api/drizzle/meta/_journal.json +++ b/platform/packages/api/drizzle/meta/_journal.json @@ -379,6 +379,13 @@ "when": 1792044332549, "tag": "0060_private_previews", "breakpoints": true + }, + { + "idx": 61, + "version": "7", + "when": 1792665459659, + "tag": "0061_email_inbound_mode", + "breakpoints": true } ] } diff --git a/platform/packages/api/src/email/cf-customer.ts b/platform/packages/api/src/email/cf-customer.ts index 2b101dc60..04772b123 100644 --- a/platform/packages/api/src/email/cf-customer.ts +++ b/platform/packages/api/src/email/cf-customer.ts @@ -124,6 +124,13 @@ export type CfSendingSubdomain = Readonly<{ enabled: boolean; }>; +/** A DNS conflict that Email Sending onboarding reports for a name, from the read-only preview. */ +export type CfSendingIssue = Readonly<{ + code: string; + /** The DNS name of the conflicting record, when Cloudflare reports one. */ + name: string | null; +}>; + export type CfCustomerClient = Readonly<{ /** Verify a user or account token against the zone's authoritative account. */ verifyToken: (accountId: string) => Promise; @@ -143,6 +150,11 @@ export type CfCustomerClient = Readonly<{ * enabled (both required on the wire); malformed entries are dropped. */ listSendingSubdomains: (zoneId: string) => Promise>; + /** + * POST /zones/{z}/email/sending/subdomains/preview — a read-only dry run of onboarding. + * Returns every issue Cloudflare reports, including records that are only missing. + */ + previewSending: (zoneId: string, name: string) => Promise>; /** Raw script GET — true when the name is taken in this account, false on 404. */ relayScriptExists: (accountId: string, scriptName: string) => Promise; /** @@ -271,6 +283,27 @@ export function createCfCustomerClient(credential: string): CfCustomerClient { return parsed; }, + previewSending: async (zoneId, name) => { + const preview = await call<{ errors?: unknown }>( + `/zones/${zoneId}/email/sending/subdomains/preview`, + { method: 'POST', body: JSON.stringify({ name }) }, + ); + const issues: Array = []; + for (const row of Array.isArray(preview?.errors) ? preview.errors : []) { + if (!row || typeof row !== 'object' || typeof row.code !== 'string') { + continue; + } + const existing = (row as { existing?: { name?: unknown } }).existing; + const multiple = (row as { multiple?: Array<{ name?: unknown }> }).multiple; + const recordName = existing?.name ?? (Array.isArray(multiple) ? multiple[0]?.name : null); + issues.push({ + code: row.code, + name: typeof recordName === 'string' ? recordName : null, + }); + } + return issues; + }, + relayScriptExists: async (accountId, scriptName) => { const response = await fetch( `${CF_CUSTOMER_API_BASE}/accounts/${accountId}/workers/scripts/${scriptName}`, diff --git a/platform/packages/api/src/email/connection-schema.ts b/platform/packages/api/src/email/connection-schema.ts index 5ef713752..7b29593fc 100644 --- a/platform/packages/api/src/email/connection-schema.ts +++ b/platform/packages/api/src/email/connection-schema.ts @@ -121,6 +121,8 @@ export const emailZoneConnections = pgTable( ); /** An exact SMTP envelope domain, with a single project assignment or shared addressing. */ +export type EmailInboundMode = 'managed' | 'none'; + export const emailDomainRoutes = pgTable( 'email_domain_routes', { @@ -137,6 +139,9 @@ export const emailDomainRoutes = pgTable( .notNull() .default('active') .$type<'active' | 'disabled' | 'removed'>(), + // 'none': outbound only. Inbound mail stays with the domain's own MX provider, so setup + // never touches Email Routing and the route never becomes inbound-ready. + inbound_mode: text('inbound_mode').notNull().default('managed').$type(), inbound_status: text('inbound_status') .notNull() .default('pending') @@ -165,6 +170,10 @@ export const emailDomainRoutes = pgTable( 'email_domain_routes_assignment_check', sql`(${table.addressing} = 'custom' AND ${table.project_id} IS NOT NULL) OR (${table.addressing} = 'shared' AND ${table.project_id} IS NULL)`, ), + check( + 'email_domain_routes_inbound_mode_check', + sql`${table.inbound_mode} = 'managed' OR (${table.inbound_mode} = 'none' AND ${table.addressing} = 'custom')`, + ), ], ); diff --git a/platform/packages/api/src/email/connection-service.ts b/platform/packages/api/src/email/connection-service.ts index 408e8d23e..aad5e41f4 100644 --- a/platform/packages/api/src/email/connection-service.ts +++ b/platform/packages/api/src/email/connection-service.ts @@ -27,6 +27,7 @@ import { emailOperations, emailOperationSteps, emailZoneConnections, + type EmailInboundMode, } from './connection-schema.ts'; import { isValidEmailDomain, normalizeEmailDomain } from './domain-name.ts'; import { RELAY_COMPATIBILITY_DATE, RELAY_SOURCE, RELAY_VERSION } from './relay-worker.ts'; @@ -160,6 +161,10 @@ const NATIVE_CUSTOM_SYNC_STEPS = [ 'sending_onboard', 'publish', ] as const; +// Outbound-only routes never deploy ingress or touch Email Routing, so the apex MX is left alone. +const OUTBOUND_ONLY_STEPS = ['sending_onboard', 'publish'] as const; +const OUTBOUND_ONLY_INBOUND_REASON = + "Outbound only: inbound mail stays with the domain's MX provider"; const REMOVE_STEPS = ['route_disable', 'catchall_detach', 'ingress_delete', 'publish'] as const; const ROTATE_STEPS = [ 'secret_put', @@ -171,6 +176,7 @@ const ROTATE_STEPS = [ type StepKind = | (typeof SETUP_STEPS)[number] | (typeof STAGED_SECRET_REPAIR_STEPS)[number] + | (typeof OUTBOUND_ONLY_STEPS)[number] | (typeof REMOVE_STEPS)[number] | (typeof SHARED_SYNC_STEPS)[number] | (typeof ROTATE_STEPS)[number]; @@ -1354,6 +1360,16 @@ export async function readStagedConnectionSecret( ); } +/** The inbound columns a route starts with, or returns to when it is reactivated. */ +function outboundOnlyInbound(inboundMode: EmailInboundMode) { + return { + inbound_mode: inboundMode, + inbound_status: inboundMode === 'none' ? ('disabled' as const) : ('pending' as const), + inbound_reason: inboundMode === 'none' ? OUTBOUND_ONLY_INBOUND_REASON : null, + inbound_observed_at: null, + }; +} + export async function requestEmailDomainAdd( db: Db, env: Bindings, @@ -1363,9 +1379,12 @@ export async function requestEmailDomainAdd( actor: EmailActor; token?: string; attemptId?: string; + inboundMode?: EmailInboundMode; }, ): Promise<{ operationId: string; routeId: string }> { const domain = normalizeEmailDomain(input.domain); + const inboundMode = input.inboundMode ?? 'managed'; + const outboundOnly = inboundMode === 'none'; if (!isValidEmailDomain(domain)) { throw new EmailOperationError('invalid email domain', 400); } @@ -1373,6 +1392,9 @@ export async function requestEmailDomainAdd( throw new EmailOperationError('system cannot assign project email domains', 403); } const actor = input.actor as Exclude; + if (outboundOnly && actor.kind !== 'admin') { + throw new EmailOperationError('outbound-only email domains are administrator-managed', 403); + } const installationId = emailInstallationId(env); const key = input.attemptId ?? generateId('eadd'); const actorScope = `${actor.kind}:${actor.userId}:${input.projectId}`; @@ -1381,6 +1403,8 @@ export async function requestEmailDomainAdd( domain, actor: input.actor.kind, tokenDigest: input.token ? await digest(input.token) : null, + // Only outbound-only requests carry the mode, so earlier managed digests still match. + ...(outboundOnly ? { inboundMode } : {}), }); const previous = await findEmailOperationByKey(db, installationId, actorScope, key); if (previous) { @@ -1411,11 +1435,19 @@ export async function requestEmailDomainAdd( } if (domain !== zone.name) { throw new EmailOperationError( - 'Cloudflare catch-all routing requires a zone apex; use the apex domain for custom inbound email', + outboundOnly + ? 'outbound-only email domains must be a zone apex' + : 'Cloudflare catch-all routing requires a zone apex; use the apex domain for custom inbound email', 400, ); } if (zone.account.id === env.EMAIL_SHARED_ACCOUNT_ID && zone.id === env.EMAIL_SHARED_ZONE_ID) { + if (outboundOnly) { + throw new EmailOperationError( + 'the installation shared mail zone receives mail and cannot be outbound-only', + 409, + ); + } if (actor.kind !== 'admin') { throw new EmailOperationError( 'the installation shared mail zone is administrator-managed', @@ -1441,7 +1473,11 @@ export async function requestEmailDomainAdd( } verifiedToken = verifiedApiTokenMetadata(verification); // The actual zone read above proves resource scope more reliably than a template URL. - await client.getRoutingSettings(zone.id); + if (outboundOnly) { + await client.listSendingSubdomains(zone.id); + } else { + await client.getRoutingSettings(zone.id); + } } const existingConnection = ( await db @@ -1457,7 +1493,10 @@ export async function requestEmailDomainAdd( .limit(1) )[0]; const existingAssignment = await resolveEmailDomain(db, domain); - if (!existingAssignment || existingAssignment.route.desired_state !== 'active') { + // Outbound-only setup never takes the apex MX or catch-all, so their owners do not matter. + const mxPreflight = + !outboundOnly && (!existingAssignment || existingAssignment.route.desired_state !== 'active'); + if (mxPreflight) { const preflight = await Promise.all([ lookupMxOverDns(domain), client.getCatchAll(zone.id), @@ -1492,7 +1531,9 @@ export async function requestEmailDomainAdd( ); } } - gatewayOrigin(env); + if (!outboundOnly) { + gatewayOrigin(env); + } const connectionId = generateId('econ'); const routeId = generateId('eroute'); const operationId = generateId('eop'); @@ -1621,6 +1662,22 @@ export async function requestEmailDomainAdd( 403, ); } + // The MX preflight was skipped for an active route. If it was removed meanwhile, + // reactivating it here could take over mail that another provider now receives. + if (!outboundOnly && !mxPreflight && existingRoute.desired_state !== 'active') { + throw new EmailOperationError('email domain changed during the request; retry', 409); + } + if ( + existingRoute.desired_state === 'active' && + existingRoute.inbound_mode !== inboundMode + ) { + throw new EmailOperationError( + existingRoute.inbound_mode === 'none' + ? 'email domain is set up as outbound-only; remove it before adding it with inbound mail' + : 'email domain receives mail through Void; remove it before adding it as outbound-only', + 409, + ); + } const blockedAssignment = ( await tx .select({ id: emailOperations.id }) @@ -1693,9 +1750,7 @@ export async function requestEmailDomainAdd( ...(reactivating ? { desired_generation: sql`${emailDomainRoutes.desired_generation} + 1`, - inbound_status: 'pending' as const, - inbound_reason: null, - inbound_observed_at: null, + ...outboundOnlyInbound(inboundMode), outbound_status: 'pending' as const, outbound_reason: null, outbound_observed_at: null, @@ -1725,16 +1780,18 @@ export async function requestEmailDomainAdd( request_digest: requestDigest, desired_generation: generation, }); - const steps = isSharedGatewayConnection(env, connection) - ? NATIVE_CUSTOM_SYNC_STEPS - : connection.staged_secret_generation !== null - ? STAGED_SECRET_REPAIR_STEPS - : connection.inbound_status !== 'ready' || - existingRoute.inbound_status !== 'ready' || - reactivating || - blockedAssignment - ? SETUP_STEPS - : SYNC_STEPS; + const steps = outboundOnly + ? OUTBOUND_ONLY_STEPS + : isSharedGatewayConnection(env, connection) + ? NATIVE_CUSTOM_SYNC_STEPS + : connection.staged_secret_generation !== null + ? STAGED_SECRET_REPAIR_STEPS + : connection.inbound_status !== 'ready' || + existingRoute.inbound_status !== 'ready' || + reactivating || + blockedAssignment + ? SETUP_STEPS + : SYNC_STEPS; const stepRoute: EmailStepRoute = { id: existingRoute.id, domain, @@ -1842,6 +1899,7 @@ export async function requestEmailDomainAdd( addressing: 'custom', managed_by: actor.kind, assigned_by_user_id: actor.userId, + ...outboundOnlyInbound(inboundMode), }); await tx.insert(emailOperations).values({ id: operationId, @@ -1857,13 +1915,15 @@ export async function requestEmailDomainAdd( request_digest: requestDigest, desired_generation: connection.desired_generation, }); - const steps = isSharedGatewayConnection(env, connection) - ? NATIVE_CUSTOM_SYNC_STEPS - : repairStaged - ? STAGED_SECRET_REPAIR_STEPS - : created || connection.inbound_status !== 'ready' - ? SETUP_STEPS - : SIBLING_STEPS; + const steps = outboundOnly + ? OUTBOUND_ONLY_STEPS + : isSharedGatewayConnection(env, connection) + ? NATIVE_CUSTOM_SYNC_STEPS + : repairStaged + ? STAGED_SECRET_REPAIR_STEPS + : created || connection.inbound_status !== 'ready' + ? SETUP_STEPS + : SIBLING_STEPS; const stepRoute: EmailStepRoute = { id: routeId, domain, desired_generation: 1 }; await insertEmailOperationSteps(tx, env, { operationId, @@ -2560,15 +2620,17 @@ async function requestExistingOperation( }); const steps = kind === 'sync' - ? isSharedGatewayConnection(env, connection) - ? route.addressing === 'shared' - ? SHARED_SYNC_STEPS - : NATIVE_CUSTOM_SYNC_STEPS - : connection.staged_secret_generation !== null - ? STAGED_SECRET_REPAIR_STEPS - : connection.inbound_status !== 'ready' || route.inbound_status !== 'ready' - ? SETUP_STEPS - : SYNC_STEPS + ? route.inbound_mode === 'none' + ? OUTBOUND_ONLY_STEPS + : isSharedGatewayConnection(env, connection) + ? route.addressing === 'shared' + ? SHARED_SYNC_STEPS + : NATIVE_CUSTOM_SYNC_STEPS + : connection.staged_secret_generation !== null + ? STAGED_SECRET_REPAIR_STEPS + : connection.inbound_status !== 'ready' || route.inbound_status !== 'ready' + ? SETUP_STEPS + : SYNC_STEPS : REMOVE_STEPS; await insertEmailOperationSteps(tx, env, { operationId: opId, @@ -2713,7 +2775,11 @@ export async function runEmailOperation(db: Db, env: Bindings, operationId: stri op.route_id ) { const [route] = await tx - .select({ projectId: emailDomainRoutes.project_id, domain: emailDomainRoutes.domain }) + .select({ + projectId: emailDomainRoutes.project_id, + domain: emailDomainRoutes.domain, + inboundMode: emailDomainRoutes.inbound_mode, + }) .from(emailDomainRoutes) .where(eq(emailDomainRoutes.id, op.route_id)) .limit(1); @@ -2742,14 +2808,21 @@ export async function runEmailOperation(db: Db, env: Bindings, operationId: stri ), ) .limit(1); + // An outbound-only route never installed a catch-all or ingress Worker, so its + // removal has nothing to clean up in Email Routing or Workers. const retained = sibling ? { retainedForSibling: true } - : !canManageConnection( - { kind: op.actor_kind, userId: op.actor_user_id ?? '' } as EmailActor, - connection, - ) - ? { retainedForManager: true } - : null; + : route?.inboundMode === 'none' + ? { outboundOnly: true } + : !canManageConnection( + { kind: op.actor_kind, userId: op.actor_user_id ?? '' } as EmailActor, + connection, + ) + ? { retainedForManager: true } + : null; + if (retained && 'outboundOnly' in retained && step.kind === 'ingress_delete') { + await forgetUnusedEmailCredential(tx, connection.id, op.route_id); + } if (retained) { // Local retention is atomic with its outcome, so a driver crash cannot // turn a step that never called the provider into an unknown write. @@ -3266,6 +3339,48 @@ async function executeEmailStep( if (!route || route.desired_state !== 'active') { return { skipped: true }; } + const outboundOnly = route.inbound_mode === 'none'; + const blockOutbound = async (reason: string, message: string): Promise => { + await db + .update(emailDomainRoutes) + .set({ + outbound_status: 'blocked', + outbound_reason: reason, + outbound_observed_at: sql`now()`, + }) + .where( + and( + eq(emailDomainRoutes.id, route.id), + eq(emailDomainRoutes.connection_id, connection.id), + eq(emailDomainRoutes.desired_generation, route.desired_generation), + eq(emailDomainRoutes.desired_state, 'active'), + ), + ); + throw new EmailOperationError(message, 409); + }; + // The apex usually belongs to another mail provider. Onboarding only adds records on + // cf-bounce. and _dmarc.; refuse before writing if any of them clash. + // Missing records are expected here, since onboarding creates them. A domain that is + // already onboarded owns those records, so a later sync skips the check. + if ( + outboundOnly && + !(await emailOperationPreflight(() => + sendingOnboardedReadback(client, connection.zone_id, route.domain), + )) + ) { + const conflicts = ( + await emailOperationPreflight(() => client.previewSending(connection.zone_id, route.domain)) + ).filter((issue) => !issue.code.endsWith('.missing')); + if (conflicts.length > 0) { + const list = conflicts + .map((issue) => (issue.name ? `${issue.code} at ${issue.name}` : issue.code)) + .join('; '); + await blockOutbound( + 'Existing DNS records conflict with Cloudflare Email Sending', + `Cloudflare Email Sending cannot onboard ${route.domain}: ${list}. Fix these DNS records, then run domain-sync.`, + ); + } + } try { await client.onboardSending(connection.zone_id, route.domain); } catch (error) { @@ -3283,6 +3398,13 @@ async function executeEmailStep( if (!definitiveRefusal) { throw error; } + if (outboundOnly) { + // Verified-only sending needs an Email Routing domain, so it is no fallback here. + await blockOutbound( + 'Cloudflare Sending onboarding unavailable', + `Cloudflare refused Email Sending for ${route.domain} (HTTP ${error.status}). It needs the Workers Paid plan; enable it, then run domain-sync.`, + ); + } await db .update(emailDomainRoutes) .set({ diff --git a/platform/packages/api/src/email/domain-service.ts b/platform/packages/api/src/email/domain-service.ts index 5fc68d7e0..da9599ba9 100644 --- a/platform/packages/api/src/email/domain-service.ts +++ b/platform/packages/api/src/email/domain-service.ts @@ -7,7 +7,12 @@ import type { Db } from '../db.ts'; import type { ApiEnv } from '../env.ts'; import type { Project } from '../types.ts'; import { createCfCustomerClient } from './cf-customer.ts'; -import { emailDomainRoutes, emailOperations, emailZoneConnections } from './connection-schema.ts'; +import { + emailDomainRoutes, + emailOperations, + emailZoneConnections, + type EmailInboundMode, +} from './connection-schema.ts'; import { EmailOperationError, credentialFor, @@ -30,6 +35,7 @@ export type EmailDomainAddInput = Readonly<{ apiToken?: string; attemptId?: unknown; managedBy: 'owner' | 'admin'; + inboundMode?: EmailInboundMode; }>; export async function getEmailDomainRowByDomain(db: Db, domain: string) { return ( @@ -52,7 +58,14 @@ export function serializeEmailRoute( domain: route.domain, projectId: route.project_id, managed_by: route.managed_by, - status: route.desired_state === 'active' ? route.inbound_status : 'disabled', + inboundMode: route.inbound_mode, + // An outbound-only route is as ready as its sending. + status: + route.desired_state !== 'active' + ? 'disabled' + : route.inbound_mode === 'none' + ? route.outbound_status + : route.inbound_status, connectionId: connection.id, account_id: connection.account_id, zone_id: connection.zone_id, @@ -152,6 +165,7 @@ export async function addEmailDomainForProject( actor: { kind: input.managedBy, userId: context.get('user').id }, token: input.apiToken, attemptId: typeof input.attemptId === 'string' ? input.attemptId : undefined, + inboundMode: input.inboundMode, }); await runEmailOperation(context.get('db'), context.env, operationId); return domainResult(context, project, input.domain, operationId); @@ -180,21 +194,28 @@ export async function getEmailDomainStatusForProject( const client = createCfCustomerClient( await credentialFor(context.env, db, snapshot.connection), ); - inboundIssue = await inspectEmailInboundRoute( - client, - snapshot.connection, - snapshot.route.domain, - snapshot.route.addressing, - ); + // An outbound-only apex keeps its own MX, so there is no inbound route to inspect. + if (snapshot.route.inbound_mode !== 'none') { + inboundIssue = await inspectEmailInboundRoute( + client, + snapshot.connection, + snapshot.route.domain, + snapshot.route.addressing, + ); + } try { const senders = await client.listSendingSubdomains(snapshot.connection.zone_id); + // Verified-only sending works from Email Routing domains, which an outbound-only + // route is not, so a domain without Sending cannot send at all. outbound = senders.some( (sender) => sender.enabled && sender.name.toLowerCase() === snapshot.route.domain, ) ? 'ready' - : 'verified_only'; + : snapshot.route.inbound_mode === 'none' + ? 'blocked' + : 'verified_only'; outboundReason = - outbound === 'verified_only' ? 'Cloudflare Sending is not enabled for this domain' : null; + outbound === 'ready' ? null : 'Cloudflare Sending is not enabled for this domain'; } catch { outboundReason = 'Cloudflare Sending status could not be verified'; } @@ -264,6 +285,11 @@ export async function getEmailDomainStatusForProject( snapshot.connection.inbound_status === 'ready' && (snapshot.route.inbound_status === 'ready' || snapshot.route.inbound_status === 'drifted'); + // A setup step may have recorded a newer Sending result after the read above. + // Keep it: for an outbound-only route a stale 'blocked' would stop all sending. + const outboundUnchanged = + currentRoute.outbound_status === snapshot.route.outbound_status && + currentRoute.outbound_observed_at === snapshot.route.outbound_observed_at; await tx .update(emailDomainRoutes) .set({ @@ -274,9 +300,14 @@ export async function getEmailDomainStatusForProject( inbound_observed_at: sql`now()`, } : {}), - outbound_status: outbound, - outbound_reason: outboundReason, - outbound_observed_at: sql`now()`, + ...(outboundUnchanged + ? { + outbound_status: outbound, + outbound_reason: outboundReason, + outbound_observed_at: sql`now()`, + } + : {}), + updated_at: sql`now()`, }) .where( and( @@ -350,6 +381,12 @@ export async function rotateEmailIngressSecretForProject( ) { return context.json({ error: 'email domain not found' }, 404); } + if (resolved.route.inbound_mode === 'none') { + return context.json( + { error: 'outbound-only email domains have no ingress secret to rotate' }, + 409, + ); + } const operationId = await requestEmailSecretRotation( context.get('db'), context.env, diff --git a/platform/packages/api/src/operator/audit.ts b/platform/packages/api/src/operator/audit.ts index 1a9c7cc86..114c8c7ca 100644 --- a/platform/packages/api/src/operator/audit.ts +++ b/platform/packages/api/src/operator/audit.ts @@ -231,6 +231,7 @@ function auditTarget( return JSON.stringify({ domain: boundedString(body.domain, 253, true), project: boundedString(body.project, 512), + ...(body.outboundOnly === true ? { outboundOnly: true } : {}), }); } if ( diff --git a/platform/packages/api/src/operator/email-routes.ts b/platform/packages/api/src/operator/email-routes.ts index a6fdb6946..4e303cc7a 100644 --- a/platform/packages/api/src/operator/email-routes.ts +++ b/platform/packages/api/src/operator/email-routes.ts @@ -674,7 +674,7 @@ export async function listOperatorEmailDomains(context: Context, selecto export async function addOperatorEmailDomain( context: Context, - input: { domain: string; project: string; apiToken?: string }, + input: { domain: string; project: string; apiToken?: string; outboundOnly?: boolean }, dryRunRequested = false, ) { const db = context.get('db'); @@ -691,8 +691,9 @@ export async function addOperatorEmailDomain( ); } const credentialKind = input.apiToken ? 'api_token' : 'platform'; + const inboundMode = input.outboundOnly ? 'none' : 'managed'; const action = 'email.domain.add'; - const target = { domain, projectId: project.id, slug: project.slug, credentialKind }; + const target = { domain, projectId: project.id, slug: project.slug, credentialKind, inboundMode }; if (dryRunRequested) { // Read-only: the engine's own MX rule, previewed over public DNS. No // Cloudflare call — the preview must never spend the credential. @@ -702,19 +703,31 @@ export async function addOperatorEmailDomain( } catch { // A failed lookup leaves the placement unknown. } + // The engine refuses to switch an active domain between inbound and outbound-only. + const modeChange = + existing?.desired_state === 'active' && existing.inbound_mode !== inboundMode; return preview(action, target, { existing: existing - ? { status: existing.inbound_status, managed_by: existing.managed_by } + ? { + status: + existing.inbound_mode === 'none' ? existing.outbound_status : existing.inbound_status, + managed_by: existing.managed_by, + inboundMode: existing.inbound_mode, + } : null, - plan: existing ? 'sync' : 'setup', + plan: modeChange ? 'refuse' : existing ? 'sync' : 'setup', mx, - effect: existing - ? `The existing assignment for ${domain} belongs to ${project.slug}; use sync to reconcile it.` - : mx === 'subdomain' - ? `${domain} already receives mail; setup will refuse it — use an unused domain apex or a platform shared email address instead.` - : credentialKind === 'platform' - ? `Registers ${domain} for ${project.slug} on the platform's own Cloudflare credential; the zone must be in the platform's account.` - : `Registers ${domain} for ${project.slug} with the supplied API token.`, + effect: modeChange + ? `${domain} is ${existing.inbound_mode === 'none' ? 'outbound-only' : 'set up to receive mail'}; setup will refuse to change that — remove it first, then add it again.` + : existing + ? `The existing assignment for ${domain} belongs to ${project.slug}; use sync to reconcile it.` + : input.outboundOnly + ? `Onboards ${domain} for ${project.slug} to Cloudflare Email Sending only; its MX records and inbound mail stay with its current mail provider.` + : mx === 'subdomain' + ? `${domain} already receives mail; setup will refuse it — use an unused domain apex or a platform shared email address instead.` + : credentialKind === 'platform' + ? `Registers ${domain} for ${project.slug} on the platform's own Cloudflare credential; the zone must be in the platform's account.` + : `Registers ${domain} for ${project.slug} with the supplied API token.`, }); } return operatorResult( @@ -722,6 +735,7 @@ export async function addOperatorEmailDomain( domain, apiToken: input.apiToken, managedBy: 'admin', + inboundMode, }), action, target, @@ -748,10 +762,13 @@ export async function syncOperatorEmailDomain( const target = { domain, projectId: project.id, slug: project.slug }; if (dryRunRequested) { return preview(action, target, { - status: row.inbound_status, + status: row.inbound_mode === 'none' ? row.outbound_status : row.inbound_status, managed_by: row.managed_by, connection_id: row.connection_id, - effect: 'Reconciles the shared zone ingress and this exact mail-domain assignment.', + effect: + row.inbound_mode === 'none' + ? 'Re-attempts Cloudflare Email Sending onboarding for this outbound-only domain; its MX records and inbound mail are not touched.' + : 'Reconciles the shared zone ingress and this exact mail-domain assignment.', }); } return operatorResult( @@ -819,6 +836,9 @@ export async function rotateOperatorEmailIngressSecret( const { domain, row, project } = await requireDomainRow(context.get('db'), rawDomain); const action = 'email.domain.rotate-secret', target = { domain, projectId: project.id, slug: project.slug, connectionId: row.connection_id }; + if (row.inbound_mode === 'none') { + throw new OperatorError(409, 'Outbound-only email domains have no ingress secret to rotate.'); + } if (dryRunRequested) { return preview(action, target, { effect: @@ -832,6 +852,13 @@ export async function rotateOperatorEmailIngressSecret( ); } +function outboundOnlyField(body: Record): boolean { + if (body.outboundOnly !== undefined && typeof body.outboundOnly !== 'boolean') { + throw new OperatorError(400, 'outboundOnly must be a boolean.', 'invalid_argument'); + } + return body.outboundOnly === true; +} + emailOperatorRoutes.get('/email/domains', async (context) => context.json(await listOperatorEmailDomains(context, context.req.query('project') || undefined)), ); @@ -844,6 +871,7 @@ emailOperatorRoutes.post('/email/domains', async (context) => { domain: stringField(body, 'domain', { maxLength: MAX_DOMAIN_LENGTH }), project: stringField(body, 'project'), apiToken: stringField(body, 'apiToken', { optional: true, maxLength: 2048 }) || undefined, + outboundOnly: outboundOnlyField(body), }, dryRun(body), ), diff --git a/platform/packages/api/test/integration/email-admission.test.ts b/platform/packages/api/test/integration/email-admission.test.ts index 3d3fb8b97..2a42f2cf4 100644 --- a/platform/packages/api/test/integration/email-admission.test.ts +++ b/platform/packages/api/test/integration/email-admission.test.ts @@ -412,6 +412,72 @@ describe('authoritative email admission', () => { expect((await getProjectEmailUsage(env.db, project.id)).attempts).toBe(0); }); + it('sends from an outbound-only domain and never accepts mail for it', async () => { + const project = await fixture(); + await env.db.insert(emailCredentials).values({ + id: 'outbound-cred', + installation_id: 'installation', + managed_by: 'admin', + manager_user_id: project.userId, + kind: 'api_token', + account_id: 'account', + zone_id: 'outbound-zone', + ciphertext: 'ciphertext', + iv: 'iv', + key_version: 'key', + }); + await env.db.insert(emailZoneConnections).values({ + id: 'outbound-connection', + installation_id: 'installation', + account_id: 'account', + zone_id: 'outbound-zone', + zone_name: 'brand.example.com', + managed_by: 'admin', + manager_user_id: project.userId, + credential_id: 'outbound-cred', + ingress_worker_name: 'gateway', + secret_ciphertext: 'ciphertext', + secret_iv: 'iv', + secret_key_version: 'key', + management_status: 'healthy', + }); + await env.db.insert(emailDomainRoutes).values({ + id: 'outbound-route', + domain: 'brand.example.com', + connection_id: 'outbound-connection', + project_id: project.id, + addressing: 'custom', + managed_by: 'admin', + assigned_by_user_id: project.userId, + inbound_mode: 'none', + inbound_status: 'disabled', + outbound_status: 'ready', + }); + + const action = await admitEmailAction(env.db, { + ...input(project.id, 'outbound-sender'), + from: 'support@brand.example.com', + transport: 'send_raw', + }); + expect(action.recipients).toMatchObject([{ state: 'reserved' }]); + await expect( + recordEmailReceipt(env.db, { + receiptId: 'outbound-only-receipt', + projectId: project.id, + status: 'received', + route: { + routeId: 'outbound-route', + routeGeneration: 1, + connectionId: 'outbound-connection', + addressing: 'custom', + domain: 'brand.example.com', + projectSlug: project.slug, + }, + }), + ).rejects.toMatchObject({ code: 'INVALID_STATE' }); + expect(await env.db.select().from(emailReceipts)).toHaveLength(0); + }); + it('does not start a reserved custom send after the stored token expires', async () => { const project = await fixture(); await env.db.insert(emailCredentials).values({ diff --git a/platform/packages/api/test/integration/email-connections.test.ts b/platform/packages/api/test/integration/email-connections.test.ts index 0a80576e7..6165e47e3 100644 --- a/platform/packages/api/test/integration/email-connections.test.ts +++ b/platform/packages/api/test/integration/email-connections.test.ts @@ -83,6 +83,11 @@ const zones = Object.fromEntries( 'occupied.example.com', 'one.example.com', 'other.example.com', + 'outbound.example.com', + 'outbound-conflict.example.com', + 'outbound-mode.example.com', + 'outbound-refused.example.com', + 'outbound-timeout.example.com', 'pending-original.example.com', 'pending-replacement.example.com', 'pending-rotation.example.com', @@ -3074,3 +3079,286 @@ describe('durable zone email connections', () => { ).rejects.toThrow('unresolved'); }); }); + +describe('outbound-only email domains', () => { + // Microsoft 365 / Google Workspace keep the apex MX; Void only onboards Email Sending. + const FOREIGN_MX = { 'outbound.example.com': ['brand-example.mail.protection.outlook.com'] }; + const INBOUND_TAGS = [ + 'mxLookup', + 'routingDns', + 'routingSettingsGet', + 'routingRulesGet', + 'catchAll', + 'catchAllGet', + 'scriptGet', + 'scriptPut', + 'secretPut', + 'scriptDelete', + 'subaddressing', + 'relayProbe', + 'gatewayProbe', + ] as const; + const inboundCalls = (calls: Parameters[0]) => + INBOUND_TAGS.flatMap((tag) => callsByTag(calls, tag)); + const stepKinds = async (operationId: string) => + ( + await env.db + .select({ kind: emailOperationSteps.kind }) + .from(emailOperationSteps) + .where(eq(emailOperationSteps.operation_id, operationId)) + .orderBy(emailOperationSteps.ordinal) + ).map((step) => step.kind); + + it('onboards only Email Sending on an apex whose MX belongs to another provider', async () => { + const admin = await createTestUser(env.db); + const project = await createTestProject(env.db, admin.id, 'outbound-only'); + const sendingSubdomains: Array<{ name: string; enabled: boolean }> = []; + const stub = stubCustomerCf({ zones, mx: FOREIGN_MX, sendingSubdomains }); + const actor = { kind: 'admin' as const, userId: admin.id }; + const added = await requestEmailDomainAdd(env.db, bindings(), { + projectId: project.id, + domain: 'outbound.example.com', + token: TOKEN, + actor, + inboundMode: 'none', + }); + await runEmailOperation(env.db, bindings(), added.operationId); + + const operation = await getEmailOperation(env.db, project.id, added.operationId); + expect(operation?.operation.status).toBe('completed'); + expect(await stepKinds(added.operationId)).toEqual(['sending_onboard', 'publish']); + const resolved = await resolveEmailDomain(env.db, 'outbound.example.com'); + expect(resolved?.route).toMatchObject({ + inbound_mode: 'none', + inbound_status: 'disabled', + inbound_reason: expect.stringContaining('Outbound only'), + outbound_status: 'ready', + managed_by: 'admin', + }); + expect(callsByTag(stub.calls, 'sendingPreview')).toHaveLength(1); + expect(callsByTag(stub.calls, 'onboardSending')).toHaveLength(1); + expect(inboundCalls(stub.calls)).toEqual([]); + await expect( + resolveEmailIngress(env.db, { + installationId: 'void-hosted', + accountId: 'customer-account', + zoneId: zones['outbound.example.com'].id, + ingressName: resolved!.connection.ingress_worker_name, + recipient: 'support@outbound.example.com', + }), + ).resolves.toBeNull(); + + // A sync keeps to the same two steps and skips the preview once the domain is onboarded. + sendingSubdomains.push({ name: 'outbound.example.com', enabled: true }); + const sync = await requestEmailDomainSync(env.db, bindings(), added.routeId, actor); + await runEmailOperation(env.db, bindings(), sync); + expect((await getEmailOperation(env.db, project.id, sync))?.operation.status).toBe('completed'); + expect(await stepKinds(sync)).toEqual(['sending_onboard', 'publish']); + expect(callsByTag(stub.calls, 'sendingPreview')).toHaveLength(1); + + // Removal never calls Email Routing or Workers, so a Sending-only token can finish it. + const removal = await requestEmailDomainRemove(env.db, bindings(), added.routeId, actor); + await runEmailOperation(env.db, bindings(), removal); + expect((await getEmailOperation(env.db, project.id, removal))?.operation.status).toBe( + 'completed', + ); + expect(inboundCalls(stub.calls)).toEqual([]); + expect((await resolveEmailDomain(env.db, 'outbound.example.com'))?.route.desired_state).toBe( + 'removed', + ); + expect((await env.db.select().from(emailZoneConnections))[0]?.credential_id).toBeNull(); + }); + + it('blocks before onboarding when the preview reports conflicting DNS records', async () => { + const admin = await createTestUser(env.db); + const project = await createTestProject(env.db, admin.id, 'outbound-conflict'); + const previewErrors: Array<{ code: string; existing?: { name: string } }> = [ + { code: 'dmarc.missing' }, + { code: 'dmarc.multiple', existing: { name: '_dmarc.outbound-conflict.example.com' } }, + ]; + const stub = stubCustomerCf({ zones, sendingPreviewErrors: previewErrors }); + const actor = { kind: 'admin' as const, userId: admin.id }; + const added = await requestEmailDomainAdd(env.db, bindings(), { + projectId: project.id, + domain: 'outbound-conflict.example.com', + token: TOKEN, + actor, + inboundMode: 'none', + }); + await runEmailOperation(env.db, bindings(), added.operationId); + + const operation = (await getEmailOperation(env.db, project.id, added.operationId))?.operation; + expect(operation).toMatchObject({ status: 'blocked', current_step: 'sending_onboard' }); + expect(operation?.failure_detail).toContain( + 'dmarc.multiple at _dmarc.outbound-conflict.example.com', + ); + expect(operation?.failure_detail).not.toContain('dmarc.missing'); + expect(callsByTag(stub.calls, 'onboardSending')).toEqual([]); + expect( + (await resolveEmailDomain(env.db, 'outbound-conflict.example.com'))?.route, + ).toMatchObject({ outbound_status: 'blocked', inbound_status: 'disabled' }); + + previewErrors.splice(1); + const resumed = await requestEmailDomainSync(env.db, bindings(), added.routeId, actor); + expect(resumed).toBe(added.operationId); + await runEmailOperation(env.db, bindings(), resumed); + expect((await getEmailOperation(env.db, project.id, resumed))?.operation.status).toBe( + 'completed', + ); + expect( + (await resolveEmailDomain(env.db, 'outbound-conflict.example.com'))?.route.outbound_status, + ).toBe('ready'); + }); + + it('blocks instead of falling back to verified-only sending when Cloudflare refuses', async () => { + const admin = await createTestUser(env.db); + const project = await createTestProject(env.db, admin.id, 'outbound-refused'); + const failures: NonNullable[0]>['failOn'] = { + onboardSending: { message: 'entitlement required', status: 403 }, + }; + const stub = stubCustomerCf({ zones, failOn: failures }); + const actor = { kind: 'admin' as const, userId: admin.id }; + const added = await requestEmailDomainAdd(env.db, bindings(), { + projectId: project.id, + domain: 'outbound-refused.example.com', + token: TOKEN, + actor, + inboundMode: 'none', + }); + await runEmailOperation(env.db, bindings(), added.operationId); + + expect( + (await getEmailOperation(env.db, project.id, added.operationId))?.operation, + ).toMatchObject({ + status: 'blocked', + current_step: 'sending_onboard', + failure_detail: expect.stringContaining('Workers Paid'), + }); + expect((await resolveEmailDomain(env.db, 'outbound-refused.example.com'))?.route).toMatchObject( + { + outbound_status: 'blocked', + outbound_reason: 'Cloudflare Sending onboarding unavailable', + }, + ); + + delete failures.onboardSending; + const resumed = await requestEmailDomainSync(env.db, bindings(), added.routeId, actor); + await runEmailOperation(env.db, bindings(), resumed); + expect( + (await resolveEmailDomain(env.db, 'outbound-refused.example.com'))?.route.outbound_status, + ).toBe('ready'); + expect(callsByTag(stub.calls, 'onboardSending')).toHaveLength(2); + }); + + it('still falls back to verified-only sending for a domain that receives mail', async () => { + const owner = await createTestUser(env.db); + const project = await createTestProject(env.db, owner.id, 'managed-refused'); + stubCustomerCf({ + zones, + failOn: { onboardSending: { message: 'entitlement required', status: 403 } }, + }); + const added = await requestEmailDomainAdd(env.db, bindings(), { + projectId: project.id, + domain: 'refused.example.com', + token: TOKEN, + actor: { kind: 'owner', userId: owner.id }, + }); + await runEmailOperation(env.db, bindings(), added.operationId); + expect((await getEmailOperation(env.db, project.id, added.operationId))?.operation.status).toBe( + 'completed', + ); + expect((await resolveEmailDomain(env.db, 'refused.example.com'))?.route).toMatchObject({ + inbound_mode: 'managed', + inbound_status: 'ready', + outbound_status: 'verified_only', + }); + }); + + it('keeps a timed-out outbound-only onboarding unknown', async () => { + const admin = await createTestUser(env.db); + const project = await createTestProject(env.db, admin.id, 'outbound-timeout'); + stubCustomerCf({ + zones, + failOn: { onboardSending: { message: 'write outcome unavailable', status: 504 } }, + }); + const added = await requestEmailDomainAdd(env.db, bindings(), { + projectId: project.id, + domain: 'outbound-timeout.example.com', + token: TOKEN, + actor: { kind: 'admin', userId: admin.id }, + inboundMode: 'none', + }); + await runEmailOperation(env.db, bindings(), added.operationId); + expect( + (await getEmailOperation(env.db, project.id, added.operationId))?.operation, + ).toMatchObject({ + status: 'unknown', + current_step: 'sending_onboard', + failure_code: 'outcome_unknown', + }); + expect( + (await resolveEmailDomain(env.db, 'outbound-timeout.example.com'))?.route.outbound_status, + ).toBe('pending'); + }); + + it('refuses owners, the shared mail zone, subdomains and a mode change on an active route', async () => { + const admin = await createTestUser(env.db); + const project = await createTestProject(env.db, admin.id, 'outbound-mode'); + const stub = stubCustomerCf({ zones }); + await expect( + requestEmailDomainAdd(env.db, bindings(), { + projectId: project.id, + domain: 'outbound-mode.example.com', + token: TOKEN, + actor: { kind: 'owner', userId: admin.id }, + inboundMode: 'none', + }), + ).rejects.toThrow('administrator-managed'); + const actor = { kind: 'admin' as const, userId: admin.id }; + await expect( + requestEmailDomainAdd(env.db, bindings(), { + projectId: project.id, + domain: 'news.outbound-mode.example.com', + token: TOKEN, + actor, + inboundMode: 'none', + }), + ).rejects.toThrow('must be a zone apex'); + const shared = { + ...bindings(), + EMAIL_SHARED_ACCOUNT_ID: 'customer-account', + EMAIL_SHARED_ZONE_ID: zones['outbound-mode.example.com'].id, + } as ApiEnv['Bindings']; + await expect( + requestEmailDomainAdd(env.db, shared, { + projectId: project.id, + domain: 'outbound-mode.example.com', + token: TOKEN, + actor, + inboundMode: 'none', + }), + ).rejects.toThrow('cannot be outbound-only'); + expect(stub.calls.filter((call) => call.method !== 'GET')).toEqual([]); + + const added = await requestEmailDomainAdd(env.db, bindings(), { + projectId: project.id, + domain: 'outbound-mode.example.com', + token: TOKEN, + actor, + inboundMode: 'none', + }); + await runEmailOperation(env.db, bindings(), added.operationId); + await expect( + requestEmailDomainAdd(env.db, bindings(), { + projectId: project.id, + domain: 'outbound-mode.example.com', + token: TOKEN, + actor, + }), + ).rejects.toThrow('set up as outbound-only'); + expect(stub.calls.filter((call) => call.method !== 'GET').map((call) => call.tag)).toEqual([ + 'sendingPreview', + 'onboardSending', + ]); + }); +}); diff --git a/platform/packages/api/test/integration/email-domain-cf-stub.ts b/platform/packages/api/test/integration/email-domain-cf-stub.ts index f62e2bf58..e8dc6a6f0 100644 --- a/platform/packages/api/test/integration/email-domain-cf-stub.ts +++ b/platform/packages/api/test/integration/email-domain-cf-stub.ts @@ -11,6 +11,7 @@ export type StubFailTag = | 'mxLookup' | 'routingDns' | 'onboardSending' + | 'sendingPreview' | 'sendingSubdomainsGet' | 'scriptGet' | 'scriptPut' @@ -66,6 +67,8 @@ export function stubCustomerCf( }>; /** GET sending/subdomains entries; default []. */ sendingSubdomains?: ReadonlyArray<{ name: string; enabled: boolean }>; + /** POST sending/subdomains/preview `errors`; default []. */ + sendingPreviewErrors?: ReadonlyArray<{ code: string; existing?: { name: string } }>; failOn?: Partial>; /** Tags whose configured failure is returned before the provider-side mutation occurs. */ failBeforeEffect?: ReadonlyArray; @@ -289,6 +292,12 @@ export function stubCustomerCf( return cfSuccess({}); } + if (path.endsWith('/email/sending/subdomains/preview') && method === 'POST') { + const body = typeof init?.body === 'string' ? JSON.parse(init.body) : undefined; + await record({ tag: 'sendingPreview', method, url, body }); + return fail('sendingPreview') ?? cfSuccess({ errors: config.sendingPreviewErrors ?? [] }); + } + if (path.includes('/email/sending/subdomains') && method === 'POST') { const body = typeof init?.body === 'string' ? JSON.parse(init.body) : undefined; await record({ tag: 'onboardSending', method, url, body }); diff --git a/platform/packages/api/test/integration/operator-email-domains.test.ts b/platform/packages/api/test/integration/operator-email-domains.test.ts index 357f05564..b3437eb63 100644 --- a/platform/packages/api/test/integration/operator-email-domains.test.ts +++ b/platform/packages/api/test/integration/operator-email-domains.test.ts @@ -46,6 +46,8 @@ const zones = Object.fromEntries( 'mail.example.com', 'recovery.example.com', 'blocked.example.com', + 'brand.example.com', + 'brand-race.example.com', 'deleted-cleanup.example.com', 'deleted-owner-cleanup.example.com', 'replacement-cleanup.example.com', @@ -167,6 +169,126 @@ describe('operator email domain management', () => { expect(await env.db.select().from(emailDomainRoutes)).toHaveLength(0); }); + it('adds an outbound-only apex that keeps its Microsoft 365 MX', async () => { + const owner = await createTestUser(env.db, 'outbound-owner'); + const project = await createTestProject(env.db, owner.id, 'outbound-project'); + const sendingSubdomains: Array<{ name: string; enabled: boolean }> = []; + const failOn: NonNullable[0]>['failOn'] = { + onboardSending: { message: 'entitlement required', status: 403 }, + }; + const stub = stubCustomerCf({ + zones, + sendingSubdomains, + failOn, + mx: { 'brand.example.com': ['brand-example.mail.protection.outlook.com'] }, + }); + const body = { domain: 'brand.example.com', project: project.slug, outboundOnly: true }; + expect((await request('/email/domains', { ...body, outboundOnly: 'yes' })).status).toBe(400); + + const preview = await request('/email/domains', { ...body, dryRun: true }); + expect(await preview.json()).toMatchObject({ + dryRun: true, + target: { domain: 'brand.example.com', inboundMode: 'none' }, + effect: expect.stringContaining('MX records and inbound mail stay with its current'), + }); + expect(stub.calls.filter((call) => call.method !== 'GET')).toEqual([]); + + // Without Email Sending the domain cannot send at all, so setup fails instead of degrading. + const blocked = await request('/email/domains', body); + expect(await blocked.json()).toMatchObject({ + ok: false, + domain: { + inboundMode: 'none', + operationStatus: 'blocked', + readiness: { inbound: { status: 'disabled' }, outbound: { status: 'blocked' } }, + }, + }); + expect(callsByTag(stub.calls, 'routingDns')).toEqual([]); + expect(callsByTag(stub.calls, 'scriptPut')).toEqual([]); + expect(callsByTag(stub.calls, 'catchAll')).toEqual([]); + + delete failOn.onboardSending; + const synced = await request('/email/domains/brand.example.com/sync', {}); + expect(await synced.json()).toMatchObject({ + domain: { + inboundMode: 'none', + status: 'ready', + operationStatus: 'completed', + readiness: { inbound: { status: 'disabled' }, outbound: { status: 'ready' } }, + }, + }); + sendingSubdomains.push({ name: 'brand.example.com', enabled: true }); + const status = await request('/email/domains/brand.example.com'); + expect(await status.json()).toMatchObject({ + inboundMode: 'none', + status: 'ready', + readiness: { + inbound: { status: 'disabled' }, + outbound: { status: 'ready' }, + management: { status: 'healthy' }, + }, + }); + expect(callsByTag(stub.calls, 'catchAllGet')).toEqual([]); + expect( + await ( + await request('/email/domains', { + domain: 'brand.example.com', + project: project.slug, + dryRun: true, + }) + ).json(), + ).toMatchObject({ plan: 'refuse', existing: { inboundMode: 'none', status: 'ready' } }); + expect( + await (await request('/email/domains/brand.example.com/sync', { dryRun: true })).json(), + ).toMatchObject({ effect: expect.stringContaining('Email Sending onboarding') }); + expect( + (await request('/email/domains/brand.example.com/rotate-secret', { dryRun: true })).status, + ).toBe(409); + expect((await request('/email/domains/brand.example.com/rotate-secret', {})).status).toBe(409); + expect(callsByTag(stub.calls, 'secretPut')).toEqual([]); + expect( + ( + await env.db.select().from(operatorEvents).where(eq(operatorEvents.action, 'email/domains')) + ).map((event) => JSON.parse(event.target)), + ).toContainEqual({ domain: 'brand.example.com', project: project.slug, outboundOnly: true }); + + expect((await request('/email/domains/brand.example.com/remove', {})).status).toBe(200); + expect((await env.db.select().from(emailDomainRoutes))[0]?.desired_state).toBe('removed'); + expect(callsByTag(stub.calls, 'routingDns')).toEqual([]); + }); + + it('keeps a newer Sending result when a status refresh read it stale', async () => { + const owner = await createTestUser(env.db, 'outbound-race-owner'); + const project = await createTestProject(env.db, owner.id, 'outbound-race-project'); + const on: NonNullable[0]>['on'] = {}; + stubCustomerCf({ zones, on }); + await request('/email/domains', { + domain: 'brand-race.example.com', + project: project.slug, + outboundOnly: true, + }); + await env.db + .update(emailDomainRoutes) + .set({ outbound_status: 'pending', outbound_observed_at: null }) + .where(eq(emailDomainRoutes.domain, 'brand-race.example.com')); + // The refresh reads an empty Sending list, then a setup step records 'ready'. + on.sendingSubdomainsGet = async () => { + await env.db + .update(emailDomainRoutes) + .set({ outbound_status: 'ready', outbound_observed_at: new Date().toISOString() }) + .where(eq(emailDomainRoutes.domain, 'brand-race.example.com')); + }; + expect((await request('/email/domains/brand-race.example.com')).status).toBe(200); + expect( + ( + await env.db + .select() + .from(emailDomainRoutes) + .where(eq(emailDomainRoutes.domain, 'brand-race.example.com')) + )[0]?.outbound_status, + ).toBe('ready'); + }); + it('leaves a foreign catch-all untouched and reports the refusal', async () => { const owner = await createTestUser(env.db, 'foreign-owner'), project = await createTestProject(env.db, owner.id, 'foreign-project'); diff --git a/platform/packages/api/test/unit/admin-upgrade-compatibility.test.ts b/platform/packages/api/test/unit/admin-upgrade-compatibility.test.ts index e07bd8dac..5fdc0231b 100644 --- a/platform/packages/api/test/unit/admin-upgrade-compatibility.test.ts +++ b/platform/packages/api/test/unit/admin-upgrade-compatibility.test.ts @@ -303,3 +303,59 @@ it('retains rollback to 0.27.0 after adding registered OAuth clients', () => { schemaDigest, }); }); + +// 1.1.11 through 1.2.1 ignore email_domain_routes.inbound_mode and insert managed routes. +it('upgrades the 1.1.11 through 1.2.1 schema to outbound-only email routes', async () => { + const root = resolve(import.meta.dirname, '../../drizzle'); + const journal = JSON.parse(readFileSync(resolve(root, 'meta/_journal.json'), 'utf8')) as { + entries: Array<{ idx: number; tag: string; when: number }>; + }; + const compatibility = JSON.parse(readFileSync(resolve(root, 'compatibility.json'), 'utf8')); + const previous = journal.entries.filter((entry) => entry.idx < 61); + expect(previous.at(-1)?.tag).toBe('0060_private_previews'); + const schemaDigest = platformDatabaseSchemaDigest( + compatibility.lineage, + previous.map((entry) => ({ + index: entry.idx, + tag: entry.tag, + timestampMillis: entry.when, + sha256: createHash('sha256') + .update(readFileSync(resolve(root, entry.tag + '.sql'))) + .digest('hex'), + })), + ); + for (const platformVersion of ['1.1.11', '1.1.12', '1.2.0', '1.2.1']) { + expect(compatibility.rollbackSafeFrom).toContainEqual({ platformVersion, schemaDigest }); + } + const db = new PGlite(); + const route = (id: string, domain: string, addressing: string, project: string | null) => + `INSERT INTO email_domain_routes (id, domain, connection_id, project_id, addressing, managed_by) VALUES ('${id}', '${domain}', 'connection', ${project ? `'${project}'` : 'NULL'}, '${addressing}', 'admin');`; + try { + for (const entry of previous) { + await db.exec(readFileSync(resolve(root, entry.tag + '.sql'), 'utf8')); + } + await db.exec( + "INSERT INTO users (id, login) VALUES ('migration-user', 'migration-user'); INSERT INTO projects (id, user_id, slug) VALUES ('project', 'migration-user', 'project'); INSERT INTO email_zone_connections (id, installation_id, account_id, zone_id, zone_name, managed_by, ingress_worker_name, secret_ciphertext, secret_iv, secret_key_version) VALUES ('connection', 'installation', 'account', 'zone', 'example.com', 'admin', 'ingress', 'ciphertext', 'iv', 1);" + + route('existing', 'example.com', 'custom', 'project'), + ); + await db.exec(readFileSync(resolve(root, '0061_email_inbound_mode.sql'), 'utf8')); + // The previous runtime can still insert routes; they stay managed. + await db.exec(route('older-runtime', 'shared.example.com', 'shared', null)); + expect( + (await db.query('SELECT id, inbound_mode FROM email_domain_routes ORDER BY id')).rows, + ).toEqual([ + { id: 'existing', inbound_mode: 'managed' }, + { id: 'older-runtime', inbound_mode: 'managed' }, + ]); + await db.exec("UPDATE email_domain_routes SET inbound_mode = 'none' WHERE id = 'existing'"); + // Only custom routes can be outbound-only, and the mode has two values. + await expect( + db.exec("UPDATE email_domain_routes SET inbound_mode = 'none' WHERE id = 'older-runtime'"), + ).rejects.toThrow('email_domain_routes_inbound_mode_check'); + await expect( + db.exec("UPDATE email_domain_routes SET inbound_mode = 'other' WHERE id = 'existing'"), + ).rejects.toThrow('email_domain_routes_inbound_mode_check'); + } finally { + await db.close(); + } +}, 30_000); diff --git a/platform/packages/proxy/src/email.ts b/platform/packages/proxy/src/email.ts index a8eb9fdce..43b345508 100644 --- a/platform/packages/proxy/src/email.ts +++ b/platform/packages/proxy/src/email.ts @@ -480,7 +480,7 @@ function singleMailboxAddrSpec(value: string): string | null { * * { * projectId, aiToken, // auth (validated by middleware) - * from, // envelope sender (must be on an Email-Routing-enabled zone) + * from, // envelope sender (on an Email Sending or Email Routing zone) * recipients: string[], // unique normalized recipient addresses * mimeBase64, // raw MIME, base64-encoded * requestId // for logs and default operation identity