diff --git a/docs/guide/platform/administration/connected-apps.md b/docs/guide/platform/administration/connected-apps.md index ea0aa9b29..c6e92b19e 100644 --- a/docs/guide/platform/administration/connected-apps.md +++ b/docs/guide/platform/administration/connected-apps.md @@ -16,7 +16,7 @@ Local development permits exact HTTP callbacks on localhost when the platform's The app sends users to Void's authorization page, where Void displays its registered name, operator, callback origin, and requested permission. Sign-in permits reading the user's ID and username. It does not provide an account token or project management access. -Users can disconnect applications at `/oauth/connections`. Grants also expire after an hour and become invalid when their platform login session is revoked or their account is suspended. Applications must request a new sign-in when access expires. +Users can disconnect applications at `/oauth/connections`. Access tokens expire after an hour, and apps renew them with a refresh token. Refreshing does not extend access past the Void sign-in the user approved the app from: access ends 30 days after that sign-in, or earlier if the sign-in is revoked or the account is suspended. Applications must then ask the user to sign in again. ## Private previews @@ -48,6 +48,8 @@ Disabling the app or disconnecting its grant stops authorization for subsequent Discover endpoints at `/.well-known/oauth-authorization-server` on the platform API origin. Use authorization code flow with `scope=profile`, a random `state`, and S256 PKCE. Authenticate token exchanges using HTTP Basic client authentication. Send access tokens as Bearer credentials to `/oauth/userinfo`; its response contains `sub`, `preferred_username`, and the consented `scope`. +The code exchange also returns a `refresh_token`. To renew access, POST `grant_type=refresh_token` and `refresh_token` to `/oauth/token` with client Basic authentication. The response has a new access token and a new `refresh_token`; store the new one and discard the old. Each refresh token works once: sending a used one again revokes the connection and the user must reconnect, so send one refresh at a time per connection. Store refresh tokens as securely as access tokens. An optional space-separated `scope` requests fewer permissions than the user approved; the connection keeps the narrower set, and later refreshes cannot widen it. `deploy:approve` publish tokens never come with a refresh token, so each publish still needs its own approval. + For publishing, package the built app with [`prepareDeployArtifact`](/reference/api/deploy). POST to `/oauth/deployment-requests` using the profile token: ```ts @@ -66,6 +68,6 @@ Redirect the browser to the returned `authorization_url`. Exchange its code with Use the token to request URLs from `POST /projects/:id/deploy/upload-urls`, upload the declared assets, and send the artifact to `POST /projects/:id/deploy`. Set `x-void-deploy-idempotency-key` to `approval_id`. Deployment uses Void's managed deployment protocol and requires the platform deployment Queue. The credential cannot read secrets, mint project tokens, delete projects, or deploy different bytes. -Revoke identity grants with `POST /oauth/revoke`, authenticated as the client. No refresh token or OpenID Connect ID token is issued. +Revoke a connection with `POST /oauth/revoke`, authenticated as the client, using either its access token or its refresh token. Void does not issue an OpenID Connect ID token. If the API is protected by Cloudflare Access, the application operator must also configure a service identity permitted by that Access application's policy. Server OAuth endpoints and approved deployment requests still validate their own client, grant, and project authority; browser consent requires the user's company identity. diff --git a/platform/meta/design-docs/0091-connected-apps.md b/platform/meta/design-docs/0091-connected-apps.md index 2037bce57..80df2cd8a 100644 --- a/platform/meta/design-docs/0091-connected-apps.md +++ b/platform/meta/design-docs/0091-connected-apps.md @@ -2,7 +2,7 @@ Replace the companion callback allowlist from b829c7f7f with registered confidential OAuth clients. A client's callback origin is not an authority to receive a full API session. -Clients have exact callbacks, a hashed secret, an administrator-selected maximum scope, and an enabled flag. Profile authorization codes require explicit Void consent, S256 PKCE, client authentication, and exact callback binding. Codes are hashed, expire in five minutes, and are consumed transactionally. Profile access tokens are opaque, hashed in PostgreSQL, expire within an hour, and expose only subject and username. +Clients have exact callbacks, a hashed secret, an administrator-selected maximum scope, and an enabled flag. Profile authorization codes require explicit Void consent, S256 PKCE, client authentication, and exact callback binding. Codes are hashed, expire in five minutes, and are consumed transactionally. Profile access tokens are opaque, hashed in PostgreSQL, expire within an hour, and expose only subject and username. The code exchange also issues an opaque, hashed refresh token. Each redemption rotates it and issues a new access token on the same grant; the new token keeps the family expiry, which is the expiry of the grant's login session. A rejected redemption rolls back and leaves the token usable. Redeeming a rotated-out token revokes the grant. Publish tokens never get a refresh token. Void's existing human login flow can redirect only to its own OAuth login callback. That bridge exchanges the existing PKCE code on Void and retains the API token in a Void-only HttpOnly cookie. External applications never receive that token. State and browser proof bind consent; forms require the configured Void origin. OAuth responses are uncached and suppress referrers. @@ -10,12 +10,12 @@ A publish request commits to canonical SHA-256 of the manifest, asset manifest, The code exchange issues a fifteen-minute JWT with a new oauth-deploy audience, project, and approval UUID. Older platform runtimes reject that audience. Authentication default-denies endpoints; uploads must match declared hashes and sizes, deployment must match the digest, and its idempotency key must equal the approval UUID. Deployment Queue receipts enforce one execution with recoverable retries. Platforms without that Queue fail closed. -Grants retain the upstream human session and its Access identity attested during browser consent. Every use checks user suspension, session revocation and expiration, provider enabled/generation, current Access settings, and client/grant status. Queued execution rechecks these and project ownership before cutover; share locks fence client, grant, identity, and provider mutations during that transaction. Lock order follows configuration → client → grant → user → connection → session → approval. Client disablement takes its client lock before grants and requests. OAuth service endpoints accept an admitted Access service identity, then independently validate purpose-bound credentials. Browser consent never bypasses the human Access gate. +Grants retain the upstream human session and its Access identity attested during browser consent. Every use checks user suspension, session revocation and expiration, provider enabled/generation, current Access settings, and client/grant status. Queued execution rechecks these and project ownership before cutover; share locks fence client, grant, identity, and provider mutations during that transaction. Lock order follows configuration → client → grant → user → connection → session → approval. A refresh takes the grant FOR UPDATE in that chain, so refreshes of one grant run in turn, and locks its refresh-token row last: grant → token, the same order as the cascade when a grant is deleted. Client disablement takes its client lock before grants and requests. OAuth service endpoints accept an admitted Access service identity, then independently validate purpose-bound credentials. Browser consent never bypasses the human Access gate. Vibevoid stores only an encrypted profile token in its session and revalidates it before workspace access. Old account-token session cookies are not recognized. Logout revokes the grant. A separate sandbox builds an immutable checkpoint without platform credentials, stores its prepared output privately in R2, and is destroyed before Void approval. After callback, the server sends only approved bytes with the one-publish token; no platform token enters project code, the editing sandbox, or the publishing sandbox. -Registration and disablement use the existing admin auth, CSRF and durable intent/outcome audit. User revocation is restricted to the current subject's grants. Hourly housekeeping deletes expired requests and grants in bounded batches after a one-day retention window. The SQL migration adds tables and indexes only; native Cloudflare deployments gain no resource bindings. +Registration and disablement use the existing admin auth, CSRF and durable intent/outcome audit. User revocation is restricted to the current subject's grants. Hourly housekeeping deletes expired requests and grants in bounded batches after a one-day retention window. `oauth_grants.expires_at` is only the access token expiry, so a grant that is not revoked is kept while its refresh token family is live, and the connections page lists it. The SQL migration adds tables and indexes only; native Cloudflare deployments gain no resource bindings. -Trust remains explicit: registering a client does not attest its generated code. Approved code has the target project's runtime secrets and resources. Approval cannot prove that an operator's checkpoint label describes the submitted artifact. Cancellation or revocation does not undo code already published. Neither refresh tokens nor OpenID Connect are implemented. +Trust remains explicit: registering a client does not attest its generated code. Approved code has the target project's runtime secrets and resources. Approval cannot prove that an operator's checkpoint label describes the submitted artifact. Cancellation or revocation does not undo code already published. OpenID Connect is not implemented. OAuth asset PUTs sign If-None-Match: * and expire within the publish token lifetime. Existing objects produce 412; deployment streams every approved asset through BLAKE3 before provisioning or cutover. This prevents a client from substituting bytes with a matching MD5 or overwriting verified content through its upload URLs. Browser consent forms use same-origin referrers so Chrome retains a verifiable Origin; code redirects keep no-referrer. Wrangler’s localhost port rewrite is accepted only in explicit development bypass mode. diff --git a/platform/packages/api/drizzle/0063_oauth_refresh_tokens.sql b/platform/packages/api/drizzle/0063_oauth_refresh_tokens.sql new file mode 100644 index 000000000..f9daae02a --- /dev/null +++ b/platform/packages/api/drizzle/0063_oauth_refresh_tokens.sql @@ -0,0 +1,13 @@ +CREATE TABLE "oauth_refresh_tokens" ( + "id" text PRIMARY KEY NOT NULL, + "grant_id" text NOT NULL, + "token_hash" text NOT NULL, + "expires_at" timestamp with time zone NOT NULL, + "used_at" timestamp with time zone, + "created_at" timestamp with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +ALTER TABLE "oauth_refresh_tokens" ADD CONSTRAINT "oauth_refresh_tokens_grant_id_oauth_grants_id_fk" FOREIGN KEY ("grant_id") REFERENCES "public"."oauth_grants"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +CREATE UNIQUE INDEX "oauth_refresh_tokens_token" ON "oauth_refresh_tokens" USING btree ("token_hash");--> statement-breakpoint +CREATE INDEX "oauth_refresh_tokens_grant" ON "oauth_refresh_tokens" USING btree ("grant_id");--> statement-breakpoint +CREATE INDEX "oauth_refresh_tokens_expiry" ON "oauth_refresh_tokens" USING btree ("expires_at"); \ No newline at end of file diff --git a/platform/packages/api/drizzle/meta/0063_snapshot.json b/platform/packages/api/drizzle/meta/0063_snapshot.json new file mode 100644 index 000000000..31ce17b64 --- /dev/null +++ b/platform/packages/api/drizzle/meta/0063_snapshot.json @@ -0,0 +1,7105 @@ +{ + "id": "f0b0f522-ad00-406b-a38a-935ed8bc4f80", + "prevId": "9fc2492c-a3b5-494d-a5f5-a04e70a2656f", + "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_refresh_tokens": { + "name": "oauth_refresh_tokens", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "grant_id": { + "name": "grant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "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 + }, + "used_at": { + "name": "used_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_refresh_tokens_token": { + "name": "oauth_refresh_tokens_token", + "columns": [ + { + "expression": "token_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "oauth_refresh_tokens_grant": { + "name": "oauth_refresh_tokens_grant", + "columns": [ + { + "expression": "grant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "oauth_refresh_tokens_expiry": { + "name": "oauth_refresh_tokens_expiry", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "oauth_refresh_tokens_grant_id_oauth_grants_id_fk": { + "name": "oauth_refresh_tokens_grant_id_oauth_grants_id_fk", + "tableFrom": "oauth_refresh_tokens", + "tableTo": "oauth_grants", + "columnsFrom": ["grant_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "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'" + }, + "email_alias": { + "name": "email_alias", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "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_email_alias": { + "name": "idx_projects_email_alias", + "columns": [ + { + "expression": "email_alias", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"projects\".\"email_alias\" IS NOT NULL", + "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 04a06d852..19caa545f 100644 --- a/platform/packages/api/drizzle/meta/_journal.json +++ b/platform/packages/api/drizzle/meta/_journal.json @@ -393,6 +393,13 @@ "when": 1792672482613, "tag": "0062_project_email_alias", "breakpoints": true + }, + { + "idx": 63, + "version": "7", + "when": 1793192144623, + "tag": "0063_oauth_refresh_tokens", + "breakpoints": true } ] } diff --git a/platform/packages/api/src/auth/oauth.ts b/platform/packages/api/src/auth/oauth.ts index ddf73a0b8..e88d9397f 100644 --- a/platform/packages/api/src/auth/oauth.ts +++ b/platform/packages/api/src/auth/oauth.ts @@ -5,6 +5,7 @@ import type { ApiEnv } from '../env.ts'; import { oauthClients, oauthGrants, + oauthRefreshTokens, oauthRequests, previewTickets, users, @@ -147,11 +148,26 @@ export async function purgeExpiredOAuthRequests(db: Db) { SELECT id FROM oauth_requests WHERE expires_at < now() - interval '1 day' ORDER BY expires_at LIMIT 1000 )`); + // oauth_grants.expires_at is the access token expiry. Keep a grant while its + // refresh token family can still renew it; deleting the grant cascades to it. await db.execute(sql`DELETE FROM oauth_grants WHERE id IN ( - SELECT id FROM oauth_grants WHERE expires_at < now() - interval '1 day' + SELECT g.id FROM oauth_grants g + WHERE g.expires_at < now() - interval '1 day' + AND (g.revoked_at IS NOT NULL OR NOT EXISTS ( + SELECT 1 FROM oauth_refresh_tokens r + WHERE r.grant_id = g.id AND r.expires_at >= now() - interval '1 day' + )) + ORDER BY g.expires_at LIMIT 1000 + )`); + await db.execute(sql`DELETE FROM oauth_refresh_tokens WHERE id IN ( + SELECT id FROM oauth_refresh_tokens WHERE expires_at < now() - interval '1 day' ORDER BY expires_at LIMIT 1000 )`); } +/** Matches grants that still hold an unused, unexpired refresh token. */ +export function hasLiveRefreshToken() { + return sql`EXISTS (SELECT 1 FROM ${oauthRefreshTokens} WHERE ${oauthRefreshTokens.grant_id} = ${oauthGrants.id} AND ${oauthRefreshTokens.used_at} IS NULL AND ${oauthRefreshTokens.expires_at} > ${new Date().toISOString()})`; +} export async function activeOAuthClient(db: DbOrTx, id: string, lock = false) { const query = db .select() @@ -190,6 +206,9 @@ export async function activeOAuthGrant( id: string, companyIdentity?: ApiEnv['Variables']['companyIdentity'], lock = false, + // A refresh may follow an expired access token, so it skips that check. It + // locks the grant FOR UPDATE so concurrent refreshes of one grant run in turn. + forRefresh = false, ) { if (!lock) { return readActiveOAuthGrant(db, eq(oauthGrants.id, id), companyIdentity); @@ -214,10 +233,10 @@ export async function activeOAuthGrant( and( eq(oauthGrants.id, id), isNull(oauthGrants.revoked_at), - gt(oauthGrants.expires_at, new Date().toISOString()), + ...(forRefresh ? [] : [gt(oauthGrants.expires_at, new Date().toISOString())]), ), ); - const [grant] = await (lock ? query.for('share') : query); + const [grant] = await (lock ? query.for(forRefresh ? 'update' : 'share') : query); if (!grant) { throw new OAuthError('invalid_token', 'Application access expired or was revoked.', 401); } diff --git a/platform/packages/api/src/routes/oauth.ts b/platform/packages/api/src/routes/oauth.ts index 4b77afcde..9d975cb08 100644 --- a/platform/packages/api/src/routes/oauth.ts +++ b/platform/packages/api/src/routes/oauth.ts @@ -5,7 +5,7 @@ import { type DeployArtifact, } from '@void/deploy-core'; import { browserPageLead, escapeBrowserHtml } from '@void/platform/browser-page'; -import { and, eq, gt, isNull, desc } from 'drizzle-orm'; +import { and, eq, gt, isNull, desc, or } from 'drizzle-orm'; import { Hono, type Context } from 'hono'; import { bodyLimit } from 'hono/body-limit'; import { getCookie, setCookie, deleteCookie } from 'hono/cookie'; @@ -16,6 +16,7 @@ import { activeOAuthClient, activeOAuthGrant, authenticateOAuthClient, + hasLiveRefreshToken, oauthBearer, OAuthError, } from '../auth/oauth.ts'; @@ -24,10 +25,10 @@ import { hashLoginProof, randomLoginProof } from '../auth/proofs.ts'; import { validateHumanSession } from '../auth/sessions.ts'; import { platformBrowserPage, platformBrowserError } from '../browser-page.ts'; import { getPlatformCapabilities } from '../capabilities.ts'; -import { consumeCliAuthCode, getProjectBySlug, getUser } from '../db.ts'; +import { consumeCliAuthCode, getProjectBySlug, getUser, type DbOrTx } from '../db.ts'; import { isDevBypass, type ApiEnv } from '../env.ts'; import { MAX_SLUG_LENGTH } from '../project-names.ts'; -import { oauthClients, oauthGrants, oauthRequests } from '../schema.ts'; +import { oauthClients, oauthGrants, oauthRefreshTokens, oauthRequests } from '../schema.ts'; import { isAdminSuspended } from '../suspension.ts'; import previews from './previews.ts'; import { createUserProject } from './projects.ts'; @@ -48,7 +49,7 @@ export function oauthMetadata(context: Context) { revocation_endpoint: issuer + '/oauth/revoke', userinfo_endpoint: issuer + '/oauth/userinfo', response_types_supported: ['code'], - grant_types_supported: ['authorization_code'], + grant_types_supported: ['authorization_code', 'refresh_token'], scopes_supported: ['profile', 'deploy:approve', 'preview:deploy'], token_endpoint_auth_methods_supported: ['client_secret_basic'], revocation_endpoint_auth_methods_supported: ['client_secret_basic'], @@ -525,12 +526,127 @@ app.post('/approve', async (context) => { return context.redirect(redirect.href); }); +// Redeem and rotate a refresh token. Locks follow the documented order and take +// the grant FOR UPDATE before the token row, the same order as the cascade when +// a grant is deleted. A request rejected by a later check (scope, grant, +// session) rolls back and leaves the token usable; only a committed rotation +// marks it used. +async function refreshAccessToken( + context: Context, + clientId: string, + form: Record, +) { + if (typeof form.refresh_token !== 'string' || !form.refresh_token) { + throw new OAuthError('invalid_request', 'A refresh token is required.'); + } + const db = context.get('db'); + const invalid = () => + new OAuthError('invalid_grant', 'The refresh token is invalid, expired, or was already used.'); + const revoke = (tx: DbOrTx, grantId: string) => + tx + .update(oauthGrants) + .set({ revoked_at: new Date().toISOString() }) + .where(eq(oauthGrants.id, grantId)); + // A token's grant, hash and expiry never change, and used_at is set once. + const [found] = await db + .select({ token: oauthRefreshTokens, clientId: oauthGrants.client_id }) + .from(oauthRefreshTokens) + .innerJoin(oauthGrants, eq(oauthGrants.id, oauthRefreshTokens.grant_id)) + .where(eq(oauthRefreshTokens.token_hash, await hashLoginProof(form.refresh_token))); + if (!found || found.clientId !== clientId || Date.parse(found.token.expires_at) <= Date.now()) { + throw invalid(); + } + if (found.token.used_at) { + // A rotated-out token came back: treat it as stolen and end the grant. + await revoke(db, found.token.grant_id); + throw invalid(); + } + const token = found.token; + const outcome = await db.transaction(async (tx) => { + let grant; + try { + ({ grant } = await activeOAuthGrant( + tx, + token.grant_id, + context.get('companyIdentity'), + true, + true, + )); + } catch (error) { + // RFC 6749 section 5.2: a revoked or unusable grant is invalid_grant. + if (error instanceof OAuthError && error.code === 'invalid_token') { + throw new OAuthError('invalid_grant', error.message); + } + throw error; + } + const [current] = await tx + .select({ usedAt: oauthRefreshTokens.used_at }) + .from(oauthRefreshTokens) + .where(eq(oauthRefreshTokens.id, token.id)) + .for('update'); + if (current?.usedAt) { + // Another request redeemed this token first: the same reuse rule applies. + await revoke(tx, grant.id); + return null; + } + let scopes = grant.scopes; + if (typeof form.scope === 'string' && form.scope) { + const requested = form.scope.split(' ').filter(Boolean) as Array; + if ( + !requested.length || + new Set(requested).size !== requested.length || + requested.some((scope) => !grant.scopes.includes(scope)) + ) { + throw new OAuthError( + 'invalid_scope', + 'The requested permission exceeds the granted access.', + ); + } + scopes = requested; + } + await tx + .update(oauthRefreshTokens) + .set({ used_at: new Date().toISOString() }) + .where(eq(oauthRefreshTokens.id, token.id)); + const refreshToken = randomLoginProof(); + await tx.insert(oauthRefreshTokens).values({ + id: crypto.randomUUID(), + grant_id: grant.id, + token_hash: await hashLoginProof(refreshToken), + // Copied unchanged: rotation never extends the family's lifetime. + expires_at: token.expires_at, + }); + const accessToken = randomLoginProof(); + const expiresAt = new Date( + Math.min(Date.now() + TOKEN_SECONDS * 1000, grant.session.exp * 1000), + ).toISOString(); + await tx + .update(oauthGrants) + .set({ token_hash: await hashLoginProof(accessToken), expires_at: expiresAt, scopes }) + .where(eq(oauthGrants.id, grant.id)); + return { + access_token: accessToken, + refresh_token: refreshToken, + token_type: 'Bearer', + expires_in: Math.max(0, Math.floor((Date.parse(expiresAt) - Date.now()) / 1000)), + scope: scopes.join(' '), + }; + }); + if (!outcome) { + throw invalid(); + } + return context.json(outcome); +} + app.post('/token', async (context) => { const client = await authenticateOAuthClient( context.get('db'), context.req.header('Authorization'), ); const form = await context.req.parseBody(); + if (form.grant_type === 'refresh_token') { + return refreshAccessToken(context, client.id, form); + } if ( form.grant_type !== 'authorization_code' || typeof form.code !== 'string' || @@ -610,8 +726,17 @@ app.post('/token', async (context) => { .update(oauthGrants) .set({ token_hash: await hashLoginProof(accessToken) }) .where(eq(oauthGrants.id, grant.id)); + const refreshToken = randomLoginProof(); + await tx.insert(oauthRefreshTokens).values({ + id: crypto.randomUUID(), + grant_id: grant.id, + token_hash: await hashLoginProof(refreshToken), + // The family ends with the login session the user approved it from. + expires_at: new Date(grant.session.exp * 1000).toISOString(), + }); return context.json({ access_token: accessToken, + refresh_token: refreshToken, token_type: 'Bearer', expires_in: Math.max(0, Math.floor((Date.parse(grant.expires_at) - Date.now()) / 1000)), scope: grant.scopes.join(' '), @@ -638,16 +763,28 @@ app.post('/revoke', async (context) => { ); const body = await context.req.parseBody(); if (typeof body.token === 'string') { - await context - .get('db') - .update(oauthGrants) - .set({ revoked_at: new Date().toISOString() }) - .where( - and( - eq(oauthGrants.client_id, client.id), - eq(oauthGrants.token_hash, await hashLoginProof(body.token)), - ), - ); + const hash = await hashLoginProof(body.token); + await context.get('db').transaction(async (tx) => { + const [byAccessToken] = await tx + .update(oauthGrants) + .set({ revoked_at: new Date().toISOString() }) + .where(and(eq(oauthGrants.client_id, client.id), eq(oauthGrants.token_hash, hash))) + .returning({ id: oauthGrants.id }); + if (byAccessToken) { + return; + } + const [byRefreshToken] = await tx + .select({ grantId: oauthGrants.id }) + .from(oauthRefreshTokens) + .innerJoin(oauthGrants, eq(oauthGrants.id, oauthRefreshTokens.grant_id)) + .where(and(eq(oauthRefreshTokens.token_hash, hash), eq(oauthGrants.client_id, client.id))); + if (byRefreshToken) { + await tx + .update(oauthGrants) + .set({ revoked_at: new Date().toISOString() }) + .where(eq(oauthGrants.id, byRefreshToken.grantId)); + } + }); } return context.body(null, 200); }); @@ -745,7 +882,7 @@ app.get('/connections', async (context) => { and( eq(oauthGrants.user_id, session.sub), isNull(oauthGrants.revoked_at), - gt(oauthGrants.expires_at, new Date().toISOString()), + or(gt(oauthGrants.expires_at, new Date().toISOString()), hasLiveRefreshToken()), ), ) .orderBy(desc(oauthGrants.created_at)); diff --git a/platform/packages/api/src/schema.ts b/platform/packages/api/src/schema.ts index f6b9cae27..4fbcb2860 100644 --- a/platform/packages/api/src/schema.ts +++ b/platform/packages/api/src/schema.ts @@ -114,6 +114,33 @@ export const oauthGrants = pgTable( ], ); +export const oauthRefreshTokens = pgTable( + 'oauth_refresh_tokens', + { + id: text('id').primaryKey(), + grant_id: text('grant_id') + .notNull() + .references(() => oauthGrants.id, { onDelete: 'cascade' }), + token_hash: text('token_hash').notNull(), + // Set once from the token that started this rotation family and copied + // forward unchanged on every rotation: an absolute cap that rotation + // cannot extend. + expires_at: timestamp('expires_at', { mode: 'string', withTimezone: true }).notNull(), + // Null while this generation is the live refresh token for the grant. + // Set when rotated out; a later redemption of a used token is reuse and + // revokes the whole grant. + used_at: timestamp('used_at', { mode: 'string', withTimezone: true }), + created_at: timestamp('created_at', { mode: 'string', withTimezone: true }) + .notNull() + .defaultNow(), + }, + (table) => [ + uniqueIndex('oauth_refresh_tokens_token').on(table.token_hash), + index('oauth_refresh_tokens_grant').on(table.grant_id), + index('oauth_refresh_tokens_expiry').on(table.expires_at), + ], +); + export const oauthRequests = pgTable( 'oauth_requests', { diff --git a/platform/packages/api/test/integration/oauth.test.ts b/platform/packages/api/test/integration/oauth.test.ts index aeaa45bef..6cf62ed7f 100644 --- a/platform/packages/api/test/integration/oauth.test.ts +++ b/platform/packages/api/test/integration/oauth.test.ts @@ -1,5 +1,5 @@ import { createDeployArtifact } from '@void/deploy-core'; -import { eq } from 'drizzle-orm'; +import { eq, isNull } from 'drizzle-orm'; import { beforeAll, beforeEach, @@ -18,6 +18,7 @@ import { verifyOAuthDeployment, activeOAuthApproval, setOAuthPreviewPermission, + purgeExpiredOAuthRequests, } from '../../src/auth/oauth.ts'; import { createCodeChallenge } from '../../src/auth/pkce.ts'; import { createHumanSession, revokeConnectionSessions } from '../../src/auth/sessions.ts'; @@ -27,6 +28,7 @@ import app from '../../src/index.ts'; import { oauthClients, oauthGrants, + oauthRefreshTokens, oauthRequests, operatorEvents, users, @@ -117,6 +119,21 @@ async function exchange( }), }); } +async function refresh( + refreshToken: string, + overrides: Record = {}, + authorization = basic(), +) { + return request('/oauth/token', { + method: 'POST', + headers: { Authorization: authorization, 'Content-Type': 'application/x-www-form-urlencoded' }, + body: new URLSearchParams({ + grant_type: 'refresh_token', + refresh_token: refreshToken, + ...overrides, + }), + }); +} async function connect(extra: Record = {}) { const flow = await authorize(extra); expect(flow.response.status).toBe(200); @@ -130,7 +147,12 @@ async function connect(extra: Record = {}) { expect(token.status).toBe(200); return { code, - token: (await token.json()) as { access_token: string; scope: string; expires_in: number }, + token: (await token.json()) as { + access_token: string; + refresh_token: string; + scope: string; + expires_in: number; + }, }; } async function publishApproval(profile: string, slug: string, prepared = artifact) { @@ -911,6 +933,260 @@ describe('registered OAuth applications', () => { ).toBe(401); expect((await approve(pending.id!, pending.cookie)).status).not.toBe(302); }); + it('mints a refresh token on connect, rotates it on use, and invalidates the prior access token', async () => { + const { token } = await connect(); + expect(token.refresh_token).toBeTruthy(); + const refreshed = await refresh(token.refresh_token); + expect(refreshed.status).toBe(200); + const next = (await refreshed.json()) as { + access_token: string; + refresh_token: string; + scope: string; + expires_in: number; + }; + expect(next.access_token).not.toBe(token.access_token); + expect(next.refresh_token).not.toBe(token.refresh_token); + expect(next.scope).toBe('profile'); + expect( + ( + await request('/oauth/userinfo', { + headers: { Authorization: 'Bearer ' + token.access_token }, + }) + ).status, + ).toBe(401); + expect( + ( + await request('/oauth/userinfo', { + headers: { Authorization: 'Bearer ' + next.access_token }, + }) + ).status, + ).toBe(200); + }); + it('detects refresh token reuse and revokes the whole grant', async () => { + const { token } = await connect(); + const first = await refresh(token.refresh_token); + expect(first.status).toBe(200); + const firstBody = (await first.json()) as { access_token: string; refresh_token: string }; + // Replaying the already-rotated refresh token is treated as theft. + const replay = await refresh(token.refresh_token); + expect(replay.status).toBe(400); + expect((await replay.json()) as { error: string }).toMatchObject({ error: 'invalid_grant' }); + // The entire grant family is now revoked, including the token issued by the first refresh. + const second = await refresh(firstBody.refresh_token); + expect(second.status).toBe(400); + expect( + ( + await request('/oauth/userinfo', { + headers: { Authorization: 'Bearer ' + firstBody.access_token }, + }) + ).status, + ).toBe(401); + }); + it('serializes concurrent redemptions of one refresh token', async () => { + const { token } = await connect(); + const responses = await Promise.all([ + refresh(token.refresh_token), + refresh(token.refresh_token), + ]); + expect(responses.map((response) => response.status).sort()).toEqual([200, 400]); + // The second redemption is reuse, so the grant it raced is revoked. + const [grant] = await env.db.select().from(oauthGrants); + expect(grant!.revoked_at).not.toBeNull(); + }); + it.runIf(Boolean(process.env.VOID_TEST_DATABASE_URL))( + 'lets exactly one of many concurrent PostgreSQL redemptions rotate the token', + async () => { + const { token } = await connect(); + const responses = await Promise.all( + Array.from({ length: 6 }, () => refresh(token.refresh_token)), + ); + const statuses = responses.map((response) => response.status).sort(); + expect(statuses).toEqual([200, 400, 400, 400, 400, 400]); + expect( + await env.db.select().from(oauthRefreshTokens).where(isNull(oauthRefreshTokens.used_at)), + ).toHaveLength(1); + }, + ); + it('rejects an unknown or garbage refresh token', async () => { + expect((await refresh('not-a-real-refresh-token')).status).toBe(400); + }); + it('enforces an absolute refresh lifetime that rotation cannot extend', async () => { + const { token } = await connect(); + const [first] = await env.db.select().from(oauthRefreshTokens); + const [grant] = await env.db.select().from(oauthGrants); + expect(Date.parse(first!.expires_at)).toBeLessThanOrEqual(grant!.session.exp * 1000); + const rotated = (await (await refresh(token.refresh_token)).json()) as { + refresh_token: string; + }; + const [next] = await env.db + .select() + .from(oauthRefreshTokens) + .where(isNull(oauthRefreshTokens.used_at)); + expect(next!.id).not.toBe(first!.id); + expect(next!.expires_at).toBe(first!.expires_at); + await env.db + .update(oauthRefreshTokens) + .set({ expires_at: new Date(Date.now() - 1000).toISOString() }) + .where(eq(oauthRefreshTokens.id, next!.id)); + expect((await refresh(rotated.refresh_token)).status).toBe(400); + }); + it('refreshes after the access token has expired', async () => { + const { token } = await connect(); + await env.db.update(oauthGrants).set({ expires_at: new Date(Date.now() - 1000).toISOString() }); + const userinfo = (accessToken: string) => + request('/oauth/userinfo', { headers: { Authorization: 'Bearer ' + accessToken } }); + expect((await userinfo(token.access_token)).status).toBe(401); + const refreshed = await refresh(token.refresh_token); + expect(refreshed.status).toBe(200); + const next = (await refreshed.json()) as { access_token: string; expires_in: number }; + expect(next.expires_in).toBeGreaterThan(3000); + expect((await userinfo(next.access_token)).status).toBe(200); + }); + it('keeps the refresh token usable when a refresh is rejected', async () => { + const { token } = await connect(); + const rejected = await refresh(token.refresh_token, { scope: 'preview:deploy' }); + expect(rejected.status).toBe(400); + expect((await rejected.json()) as { error: string }).toMatchObject({ + error: 'invalid_scope', + }); + expect((await refresh(token.refresh_token)).status).toBe(200); + }); + it('keeps refreshable grants through housekeeping and lists them as connected', async () => { + const { token } = await connect(); + const longAgo = new Date(Date.now() - 2 * 24 * 60 * 60 * 1000).toISOString(); + await env.db.update(oauthGrants).set({ expires_at: longAgo }); + const connections = async () => + ( + await request('/oauth/connections', { headers: { Cookie: 'void_oauth_user=' + user.jwt } }) + ).text(); + const [grant] = await env.db.select().from(oauthGrants); + expect(await connections()).toContain(grant!.id); + await purgeExpiredOAuthRequests(env.db); + expect(await env.db.select().from(oauthGrants)).toHaveLength(1); + const refreshed = (await (await refresh(token.refresh_token)).json()) as { + refresh_token: string; + }; + + await env.db.update(oauthGrants).set({ expires_at: longAgo }); + await env.db.update(oauthRefreshTokens).set({ expires_at: longAgo }); + expect(await connections()).not.toContain(grant!.id); + await purgeExpiredOAuthRequests(env.db); + expect(await env.db.select().from(oauthGrants)).toHaveLength(0); + expect(await env.db.select().from(oauthRefreshTokens)).toHaveLength(0); + expect((await refresh(refreshed.refresh_token)).status).toBe(400); + + // A disconnected grant is removed even while its refresh token is unexpired. + const disconnected = await connect(); + await env.db.update(oauthGrants).set({ expires_at: longAgo, revoked_at: longAgo }); + await purgeExpiredOAuthRequests(env.db); + expect(await env.db.select().from(oauthGrants)).toHaveLength(0); + expect((await refresh(disconnected.token.refresh_token)).status).toBe(400); + }); + it('narrows scope on refresh and does not let a later refresh widen it back', async () => { + await setOAuthPreviewPermission(env.db, registration.client.id, true); + const { token } = await connect({ scope: 'profile preview:deploy' }); + expect(token.scope).toBe('profile preview:deploy'); + const narrowed = await refresh(token.refresh_token, { scope: 'profile' }); + expect(narrowed.status).toBe(200); + const narrowedBody = (await narrowed.json()) as { refresh_token: string; scope: string }; + expect(narrowedBody.scope).toBe('profile'); + const widenAttempt = await refresh(narrowedBody.refresh_token, { + scope: 'profile preview:deploy', + }); + expect(widenAttempt.status).toBe(400); + const [grant] = await env.db.select().from(oauthGrants); + expect(grant!.scopes).toEqual(['profile']); + }); + it('rejects a refresh token presented to a different client without spending it', async () => { + const { token } = await connect(); + const other = await registerOAuthClient(env.db, { + name: 'Other app', + operator: 'Other operator', + redirectUris: [redirect], + scopes: ['profile'], + }); + const otherBasic = 'Basic ' + btoa(other.client.id + ':' + other.clientSecret); + expect((await refresh(token.refresh_token, {}, otherBasic)).status).toBe(400); + const rotated = await refresh(token.refresh_token); + expect(rotated.status).toBe(200); + // Another client presenting the rotated-out token does not revoke the grant. + expect((await refresh(token.refresh_token, {}, otherBasic)).status).toBe(400); + const { refresh_token } = (await rotated.json()) as { refresh_token: string }; + expect((await refresh(refresh_token)).status).toBe(200); + }); + it('blocks refresh once the grant is revoked, client disabled, account suspended, or the session is invalidated', async () => { + const disconnected = await connect(); + await request('/oauth/revoke', { + method: 'POST', + headers: { Authorization: basic(), 'Content-Type': 'application/x-www-form-urlencoded' }, + body: new URLSearchParams({ token: disconnected.token.access_token }), + }); + expect((await refresh(disconnected.token.refresh_token)).status).toBe(400); + + const suspended = await connect(); + await env.db.update(users).set({ suspended_reason: 'admin' }).where(eq(users.id, user.id)); + expect((await refresh(suspended.token.refresh_token)).status).toBe(400); + await env.db.update(users).set({ suspended_reason: null }).where(eq(users.id, user.id)); + // The rejected refresh did not spend the token. + expect((await refresh(suspended.token.refresh_token)).status).toBe(200); + + const disabled = await connect(); + await disableOAuthClient(env.db, registration.client.id); + expect((await refresh(disabled.token.refresh_token)).status).toBe(401); + registration = await registerOAuthClient(env.db, { + name: 'Vibevoid', + operator: 'App operator', + redirectUris: [redirect], + scopes: ['profile', 'deploy:approve'], + }); + + await importLegacyAuthentication(env.db, env); + const human = await createHumanSession(env.db, env.JWT_SECRET, { + userId: user.id, + connectionId: 'github', + revision: 1, + generation: 1, + audience: 'api', + }); + user.jwt = human.token; + const bySession = await connect(); + await revokeConnectionSessions(env.db, 'github'); + expect((await refresh(bySession.token.refresh_token)).status).toBe(400); + }); + it('revokes a refresh token (and its grant) through the revocation endpoint', async () => { + const { token } = await connect(); + const revoked = await request('/oauth/revoke', { + method: 'POST', + headers: { Authorization: basic(), 'Content-Type': 'application/x-www-form-urlencoded' }, + body: new URLSearchParams({ token: token.refresh_token }), + }); + expect(revoked.status).toBe(200); + expect((await refresh(token.refresh_token)).status).toBe(400); + expect( + ( + await request('/oauth/userinfo', { + headers: { Authorization: 'Bearer ' + token.access_token }, + }) + ).status, + ).toBe(401); + }); + it('never issues a refresh token for a publish (deploy:approve) credential', async () => { + const { token } = await connect(); + const project = await createTestProject(env.db, user.id, 'no-refresh-app'); + const flow = await publishApproval(token.access_token, project.slug); + const consent = await approve(flow.id, flow.cookie); + const code = new URL(consent.headers.get('Location')!).searchParams.get('code')!; + const exchanged = await exchange(code, {}, basic()); + expect(exchanged.status).toBe(200); + const body = (await exchanged.json()) as { scope: string; refresh_token?: string }; + expect(body.scope).toBe('deploy:approve'); + expect(body.refresh_token).toBeUndefined(); + }); + it('advertises the refresh_token grant in authorization server metadata', async () => { + const metadata = await request('/.well-known/oauth-authorization-server'); + const body = (await metadata.json()) as { grant_types_supported: Array }; + expect(body.grant_types_supported).toContain('refresh_token'); + }); it('requires approval by the grant owner and limits a publish to the chosen artifact and project', async () => { const { token } = await connect(); const project = await createTestProject(env.db, user.id, 'approved-app');