From 9ec98c5e54b7b92a0c142ba498f7b61ae2ee4c2f Mon Sep 17 00:00:00 2001 From: Rajat Date: Sun, 25 Oct 2026 15:37:55 +0530 Subject: [PATCH 01/19] feat: OpenAPI API reference from Git (#26) Import a single-file OpenAPI 3.0/3.1 spec from the connected repository, publish one docs page per operation plus a landing page under /docs/, a lazily loaded "Try it" playground, and /openapi.json. Adds Tabs, Table, Collapsible, NativeSelect and InlineCode primitives. Co-Authored-By: Claude Opus 5.5 --- .../page-builder-api-reference-primitives.md | 5 + apps/api/drizzle/0011_openapi_reference.sql | 53 + apps/api/drizzle/meta/0011_snapshot.json | 9973 +++++++++++ apps/api/drizzle/meta/_journal.json | 7 + apps/api/package.json | 1 + apps/api/src/auth/operation-policy.ts | 15 + apps/api/src/chatbot/completion.ts | 2 +- apps/api/src/db/schema.ts | 70 +- apps/api/src/documentation/authoring.ts | 19 + apps/api/src/documentation/github/errors.ts | 4 + apps/api/src/documentation/github/routes.ts | 14 + apps/api/src/documentation/github/worker.ts | 10 + apps/api/src/documentation/improvements.ts | 4 +- .../openapi/fixtures/medialit.json | 1170 ++ .../openapi/fixtures/sendlit.json | 13857 ++++++++++++++++ .../src/documentation/openapi/import.test.ts | 192 + apps/api/src/documentation/openapi/import.ts | 698 + apps/api/src/documentation/openapi/parse.ts | 69 + apps/api/src/documentation/openapi/samples.ts | 149 + apps/api/src/documentation/openapi/schema.ts | 192 + .../src/documentation/openapi/service.test.ts | 155 + apps/api/src/documentation/openapi/service.ts | 530 + apps/api/src/documentation/queries.ts | 10 +- apps/api/src/inbox-ai/generate.ts | 4 +- apps/api/src/public/routes.ts | 56 +- apps/api/src/tickets/notify.ts | 2 +- .../app/api/webhooks/documentation/route.ts | 2 +- .../sites/[teamId]/docs/[...slug]/page.tsx | 86 +- .../docs/collections/[sectionSlug]/page.tsx | 3 +- .../app/sites/[teamId]/openapi.json/route.ts | 24 + .../api-reference/api-reference.tsx | 532 + .../components/api-reference/method-badge.tsx | 61 + .../components/api-reference/playground.tsx | 580 + .../sites/components/api-reference/request.ts | 195 + .../components/api-reference/schema-view.tsx | 264 + .../sites/components/api-reference/try-it.tsx | 88 + apps/sites/components/themed-docs.tsx | 122 +- apps/sites/lib/api.ts | 54 +- apps/sites/lib/docs-tree.ts | 50 +- .../content/api-reference-source-section.tsx | 265 + .../content/documentation-source-manage.tsx | 3 + .../content/pages-source-section.tsx | 2 +- apps/web/lib/api.ts | 17 + bun.lock | 11 +- docs/openapi-support.md | 565 + packages/api-contract/src/content-paths.ts | 5 +- packages/api-contract/src/contract.ts | 17 + .../src/documentation-github-contract.ts | 31 + packages/api-contract/src/index.ts | 1 + .../api-contract/src/schemas/api-reference.ts | 213 + packages/api-contract/src/schemas/chatbot.ts | 2 +- .../api-contract/src/schemas/documentation.ts | 2 +- .../api-contract/src/schemas/help-desk.ts | 2 +- .../src/primitives/code-snippet.tsx | 2 +- .../src/primitives/collapsible.tsx | 66 + packages/page-builder/src/primitives/index.ts | 5 + .../src/primitives/inline-code.tsx | 40 + .../src/primitives/native-select.tsx | 43 + .../page-builder/src/primitives/table.tsx | 80 + packages/page-builder/src/primitives/tabs.tsx | 96 + .../page-builder/src/server/primitives.ts | 8 +- packages/page-builder/tsup.islands.ts | 1 + 62 files changed, 30739 insertions(+), 60 deletions(-) create mode 100644 .changeset/page-builder-api-reference-primitives.md create mode 100644 apps/api/drizzle/0011_openapi_reference.sql create mode 100644 apps/api/drizzle/meta/0011_snapshot.json create mode 100644 apps/api/src/documentation/openapi/fixtures/medialit.json create mode 100644 apps/api/src/documentation/openapi/fixtures/sendlit.json create mode 100644 apps/api/src/documentation/openapi/import.test.ts create mode 100644 apps/api/src/documentation/openapi/import.ts create mode 100644 apps/api/src/documentation/openapi/parse.ts create mode 100644 apps/api/src/documentation/openapi/samples.ts create mode 100644 apps/api/src/documentation/openapi/schema.ts create mode 100644 apps/api/src/documentation/openapi/service.test.ts create mode 100644 apps/api/src/documentation/openapi/service.ts create mode 100644 apps/sites/app/sites/[teamId]/openapi.json/route.ts create mode 100644 apps/sites/components/api-reference/api-reference.tsx create mode 100644 apps/sites/components/api-reference/method-badge.tsx create mode 100644 apps/sites/components/api-reference/playground.tsx create mode 100644 apps/sites/components/api-reference/request.ts create mode 100644 apps/sites/components/api-reference/schema-view.tsx create mode 100644 apps/sites/components/api-reference/try-it.tsx create mode 100644 apps/web/components/content/api-reference-source-section.tsx create mode 100644 docs/openapi-support.md create mode 100644 packages/api-contract/src/schemas/api-reference.ts create mode 100644 packages/page-builder/src/primitives/collapsible.tsx create mode 100644 packages/page-builder/src/primitives/inline-code.tsx create mode 100644 packages/page-builder/src/primitives/native-select.tsx create mode 100644 packages/page-builder/src/primitives/table.tsx create mode 100644 packages/page-builder/src/primitives/tabs.tsx diff --git a/.changeset/page-builder-api-reference-primitives.md b/.changeset/page-builder-api-reference-primitives.md new file mode 100644 index 0000000..b5c2a80 --- /dev/null +++ b/.changeset/page-builder-api-reference-primitives.md @@ -0,0 +1,5 @@ +--- +"@frontlit/page-builder": minor +--- + +Add `Tabs`, `Table`, `Collapsible`, `NativeSelect` and `InlineCode` primitives (all themed; `Tabs` is a client island, the rest are server-safe), and export `CodeSnippet` from `@frontlit/page-builder/primitives/server`. diff --git a/apps/api/drizzle/0011_openapi_reference.sql b/apps/api/drizzle/0011_openapi_reference.sql new file mode 100644 index 0000000..98b84f7 --- /dev/null +++ b/apps/api/drizzle/0011_openapi_reference.sql @@ -0,0 +1,53 @@ +CREATE TABLE IF NOT EXISTS "documentation_api_operations" ( + "spec_id" text NOT NULL, + "team_id" text NOT NULL, + "route" text NOT NULL, + "title" text NOT NULL, + "group" text NOT NULL, + "order" integer NOT NULL, + "method" text NOT NULL, + "path" text NOT NULL, + "deprecated" boolean DEFAULT false NOT NULL, + "slice" text NOT NULL, + "chunks" jsonb NOT NULL +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "documentation_api_specs" ( + "spec_id" text PRIMARY KEY NOT NULL, + "team_id" text NOT NULL, + "status" text NOT NULL, + "commit_sha" text NOT NULL, + "source_path" text NOT NULL, + "source_blob_sha" text, + "mount" text NOT NULL, + "spec" text, + "spec_hash" text, + "info" jsonb, + "diagnostics" jsonb DEFAULT '[]'::jsonb NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL +); +--> statement-breakpoint +ALTER TABLE "documentation_git_bindings" ADD COLUMN "api_spec_path" text;--> statement-breakpoint +ALTER TABLE "documentation_git_bindings" ADD COLUMN "api_mount" text DEFAULT 'api-reference' NOT NULL;--> statement-breakpoint +ALTER TABLE "documentation_settings" ADD COLUMN "active_api_spec_id" text;--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "documentation_api_operations" ADD CONSTRAINT "documentation_api_operations_spec_id_documentation_api_specs_spec_id_fk" FOREIGN KEY ("spec_id") REFERENCES "public"."documentation_api_specs"("spec_id") ON DELETE cascade ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "documentation_api_operations" ADD CONSTRAINT "documentation_api_operations_team_id_teams_id_fk" FOREIGN KEY ("team_id") REFERENCES "public"."teams"("id") ON DELETE cascade ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "documentation_api_specs" ADD CONSTRAINT "documentation_api_specs_team_id_teams_id_fk" FOREIGN KEY ("team_id") REFERENCES "public"."teams"("id") ON DELETE cascade ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +CREATE UNIQUE INDEX IF NOT EXISTS "documentation_api_operations_route_idx" ON "documentation_api_operations" USING btree ("spec_id","route");--> statement-breakpoint +CREATE INDEX IF NOT EXISTS "documentation_api_operations_team_idx" ON "documentation_api_operations" USING btree ("team_id");--> statement-breakpoint +CREATE INDEX IF NOT EXISTS "documentation_api_specs_team_idx" ON "documentation_api_specs" USING btree ("team_id","status"); \ No newline at end of file diff --git a/apps/api/drizzle/meta/0011_snapshot.json b/apps/api/drizzle/meta/0011_snapshot.json new file mode 100644 index 0000000..52e8f66 --- /dev/null +++ b/apps/api/drizzle/meta/0011_snapshot.json @@ -0,0 +1,9973 @@ +{ + "id": "1f6929b9-8047-4d61-862d-59f4a85043ac", + "prevId": "f1ffa20b-9dc5-47ee-9502-651fb066f8f5", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.accounts": { + "name": "accounts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "auth_user_id": { + "name": "auth_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email": { + "name": "email", + "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": { + "accounts_auth_user_id_auth_user_id_fk": { + "name": "accounts_auth_user_id_auth_user_id_fk", + "tableFrom": "accounts", + "tableTo": "auth_user", + "columnsFrom": [ + "auth_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "accounts_auth_user_id_unique": { + "name": "accounts_auth_user_id_unique", + "nullsNotDistinct": false, + "columns": [ + "auth_user_id" + ] + }, + "accounts_email_unique": { + "name": "accounts_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ai_settings": { + "name": "ai_settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model_settings": { + "name": "model_settings", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "region": { + "name": "region", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "azure_endpoint": { + "name": "azure_endpoint", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_api_key": { + "name": "encrypted_api_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "embedding_provider": { + "name": "embedding_provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_embedding_api_key": { + "name": "encrypted_embedding_api_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "embedding_deployment": { + "name": "embedding_deployment", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "embedding_endpoint": { + "name": "embedding_endpoint", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "index_articles": { + "name": "index_articles", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "index_changelog": { + "name": "index_changelog", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": 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": {}, + "foreignKeys": { + "ai_settings_team_id_teams_id_fk": { + "name": "ai_settings_team_id_teams_id_fk", + "tableFrom": "ai_settings", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "ai_settings_team_id_unique": { + "name": "ai_settings_team_id_unique", + "nullsNotDistinct": false, + "columns": [ + "team_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ai_usage_daily": { + "name": "ai_usage_daily", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "usage_date": { + "name": "usage_date", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reserved_tokens": { + "name": "reserved_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "used_tokens": { + "name": "used_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "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": { + "ai_usage_daily_team_date_idx": { + "name": "ai_usage_daily_team_date_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "usage_date", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ai_usage_daily_team_id_teams_id_fk": { + "name": "ai_usage_daily_team_id_teams_id_fk", + "tableFrom": "ai_usage_daily", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.api_keys": { + "name": "api_keys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key_hash": { + "name": "key_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key_prefix": { + "name": "key_prefix", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "api_keys_team_idx": { + "name": "api_keys_team_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "api_keys_team_id_teams_id_fk": { + "name": "api_keys_team_id_teams_id_fk", + "tableFrom": "api_keys", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "api_keys_key_hash_unique": { + "name": "api_keys_key_hash_unique", + "nullsNotDistinct": false, + "columns": [ + "key_hash" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.auth_account": { + "name": "auth_account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "auth_account_user_id_idx": { + "name": "auth_account_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "auth_account_user_id_auth_user_id_fk": { + "name": "auth_account_user_id_auth_user_id_fk", + "tableFrom": "auth_account", + "tableTo": "auth_user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.auth_oauth_access_token": { + "name": "auth_oauth_access_token", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "authorization_code_id": { + "name": "authorization_code_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resources": { + "name": "resources", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "requested_user_info_claims": { + "name": "requested_user_info_claims", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "refresh_id": { + "name": "refresh_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "scopes": { + "name": "scopes", + "type": "text[]", + "primaryKey": false, + "notNull": true + }, + "revoked": { + "name": "revoked", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "confirmation": { + "name": "confirmation", + "type": "jsonb", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "auth_oauth_access_token_client_id_idx": { + "name": "auth_oauth_access_token_client_id_idx", + "columns": [ + { + "expression": "client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "auth_oauth_access_token_session_id_idx": { + "name": "auth_oauth_access_token_session_id_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "auth_oauth_access_token_user_id_idx": { + "name": "auth_oauth_access_token_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "auth_oauth_access_token_authorization_code_id_idx": { + "name": "auth_oauth_access_token_authorization_code_id_idx", + "columns": [ + { + "expression": "authorization_code_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "auth_oauth_access_token_refresh_id_idx": { + "name": "auth_oauth_access_token_refresh_id_idx", + "columns": [ + { + "expression": "refresh_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "auth_oauth_access_token_client_id_auth_oauth_client_client_id_fk": { + "name": "auth_oauth_access_token_client_id_auth_oauth_client_client_id_fk", + "tableFrom": "auth_oauth_access_token", + "tableTo": "auth_oauth_client", + "columnsFrom": [ + "client_id" + ], + "columnsTo": [ + "client_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "auth_oauth_access_token_session_id_auth_session_id_fk": { + "name": "auth_oauth_access_token_session_id_auth_session_id_fk", + "tableFrom": "auth_oauth_access_token", + "tableTo": "auth_session", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "auth_oauth_access_token_user_id_auth_user_id_fk": { + "name": "auth_oauth_access_token_user_id_auth_user_id_fk", + "tableFrom": "auth_oauth_access_token", + "tableTo": "auth_user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "auth_oauth_access_token_refresh_id_auth_oauth_refresh_token_id_fk": { + "name": "auth_oauth_access_token_refresh_id_auth_oauth_refresh_token_id_fk", + "tableFrom": "auth_oauth_access_token", + "tableTo": "auth_oauth_refresh_token", + "columnsFrom": [ + "refresh_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "auth_oauth_access_token_token_unique": { + "name": "auth_oauth_access_token_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.auth_oauth_client": { + "name": "auth_oauth_client", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_secret": { + "name": "client_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "client_discovery_id": { + "name": "client_discovery_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "disabled": { + "name": "disabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "skip_consent": { + "name": "skip_consent", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "enable_end_session": { + "name": "enable_end_session", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "subject_type": { + "name": "subject_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scopes": { + "name": "scopes", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "client_credentials_scopes": { + "name": "client_credentials_scopes", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "uri": { + "name": "uri", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "contacts": { + "name": "contacts", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "tos": { + "name": "tos", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "policy": { + "name": "policy", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "software_id": { + "name": "software_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "software_version": { + "name": "software_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "software_statement": { + "name": "software_statement", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "redirect_uris": { + "name": "redirect_uris", + "type": "text[]", + "primaryKey": false, + "notNull": true + }, + "post_logout_redirect_uris": { + "name": "post_logout_redirect_uris", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "backchannel_logout_uri": { + "name": "backchannel_logout_uri", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "backchannel_logout_session_required": { + "name": "backchannel_logout_session_required", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "token_endpoint_auth_method": { + "name": "token_endpoint_auth_method", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "application_type": { + "name": "application_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "jwks": { + "name": "jwks", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "jwks_uri": { + "name": "jwks_uri", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "grant_types": { + "name": "grant_types", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "response_types": { + "name": "response_types", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "public": { + "name": "public", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "require_pkce": { + "name": "require_pkce", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "dpop_bound_access_tokens": { + "name": "dpop_bound_access_tokens", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "auth_oauth_client_user_id_idx": { + "name": "auth_oauth_client_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "auth_oauth_client_user_id_auth_user_id_fk": { + "name": "auth_oauth_client_user_id_auth_user_id_fk", + "tableFrom": "auth_oauth_client", + "tableTo": "auth_user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "auth_oauth_client_client_id_unique": { + "name": "auth_oauth_client_client_id_unique", + "nullsNotDistinct": false, + "columns": [ + "client_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.auth_oauth_client_assertion": { + "name": "auth_oauth_client_assertion", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.auth_oauth_client_resource": { + "name": "auth_oauth_client_resource", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "auth_oauth_client_resource_client_id_idx": { + "name": "auth_oauth_client_resource_client_id_idx", + "columns": [ + { + "expression": "client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "auth_oauth_client_resource_resource_id_idx": { + "name": "auth_oauth_client_resource_resource_id_idx", + "columns": [ + { + "expression": "resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "auth_oauth_client_resource_client_id_resource_id_idx": { + "name": "auth_oauth_client_resource_client_id_resource_id_idx", + "columns": [ + { + "expression": "client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "auth_oauth_client_resource_client_id_auth_oauth_client_client_id_fk": { + "name": "auth_oauth_client_resource_client_id_auth_oauth_client_client_id_fk", + "tableFrom": "auth_oauth_client_resource", + "tableTo": "auth_oauth_client", + "columnsFrom": [ + "client_id" + ], + "columnsTo": [ + "client_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "auth_oauth_client_resource_resource_id_auth_oauth_resource_identifier_fk": { + "name": "auth_oauth_client_resource_resource_id_auth_oauth_resource_identifier_fk", + "tableFrom": "auth_oauth_client_resource", + "tableTo": "auth_oauth_resource", + "columnsFrom": [ + "resource_id" + ], + "columnsTo": [ + "identifier" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.auth_oauth_consent": { + "name": "auth_oauth_consent", + "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": false + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resources": { + "name": "resources", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "requested_user_info_claims": { + "name": "requested_user_info_claims", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "scopes": { + "name": "scopes", + "type": "text[]", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "auth_oauth_consent_client_id_idx": { + "name": "auth_oauth_consent_client_id_idx", + "columns": [ + { + "expression": "client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "auth_oauth_consent_user_id_idx": { + "name": "auth_oauth_consent_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "auth_oauth_consent_client_id_auth_oauth_client_client_id_fk": { + "name": "auth_oauth_consent_client_id_auth_oauth_client_client_id_fk", + "tableFrom": "auth_oauth_consent", + "tableTo": "auth_oauth_client", + "columnsFrom": [ + "client_id" + ], + "columnsTo": [ + "client_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "auth_oauth_consent_user_id_auth_user_id_fk": { + "name": "auth_oauth_consent_user_id_auth_user_id_fk", + "tableFrom": "auth_oauth_consent", + "tableTo": "auth_user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.auth_oauth_refresh_token": { + "name": "auth_oauth_refresh_token", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "authorization_code_id": { + "name": "authorization_code_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resources": { + "name": "resources", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "requested_user_info_claims": { + "name": "requested_user_info_claims", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "revoked": { + "name": "revoked", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "rotated_at": { + "name": "rotated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "rotation_replay_response": { + "name": "rotation_replay_response", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rotation_replay_expires_at": { + "name": "rotation_replay_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "auth_time": { + "name": "auth_time", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "confirmation": { + "name": "confirmation", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "scopes": { + "name": "scopes", + "type": "text[]", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "auth_oauth_refresh_token_client_id_idx": { + "name": "auth_oauth_refresh_token_client_id_idx", + "columns": [ + { + "expression": "client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "auth_oauth_refresh_token_authorization_code_id_idx": { + "name": "auth_oauth_refresh_token_authorization_code_id_idx", + "columns": [ + { + "expression": "authorization_code_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "auth_oauth_refresh_token_session_id_idx": { + "name": "auth_oauth_refresh_token_session_id_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "auth_oauth_refresh_token_user_id_idx": { + "name": "auth_oauth_refresh_token_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "auth_oauth_refresh_token_client_id_auth_oauth_client_client_id_fk": { + "name": "auth_oauth_refresh_token_client_id_auth_oauth_client_client_id_fk", + "tableFrom": "auth_oauth_refresh_token", + "tableTo": "auth_oauth_client", + "columnsFrom": [ + "client_id" + ], + "columnsTo": [ + "client_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "auth_oauth_refresh_token_session_id_auth_session_id_fk": { + "name": "auth_oauth_refresh_token_session_id_auth_session_id_fk", + "tableFrom": "auth_oauth_refresh_token", + "tableTo": "auth_session", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "auth_oauth_refresh_token_user_id_auth_user_id_fk": { + "name": "auth_oauth_refresh_token_user_id_auth_user_id_fk", + "tableFrom": "auth_oauth_refresh_token", + "tableTo": "auth_user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "auth_oauth_refresh_token_token_unique": { + "name": "auth_oauth_refresh_token_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.auth_oauth_resource": { + "name": "auth_oauth_resource", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token_ttl": { + "name": "access_token_ttl", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "refresh_token_ttl": { + "name": "refresh_token_ttl", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "signing_algorithm": { + "name": "signing_algorithm", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "signing_key_id": { + "name": "signing_key_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "allowed_scopes": { + "name": "allowed_scopes", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "custom_claims": { + "name": "custom_claims", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "dpop_bound_access_tokens_required": { + "name": "dpop_bound_access_tokens_required", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "disabled": { + "name": "disabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "policy_version": { + "name": "policy_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "auth_oauth_resource_identifier_unique": { + "name": "auth_oauth_resource_identifier_unique", + "nullsNotDistinct": false, + "columns": [ + "identifier" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.auth_session": { + "name": "auth_session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "reauthenticated_at": { + "name": "reauthenticated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "auth_session_user_id_idx": { + "name": "auth_session_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "auth_session_user_id_auth_user_id_fk": { + "name": "auth_session_user_id_auth_user_id_fk", + "tableFrom": "auth_session", + "tableTo": "auth_user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "auth_session_token_unique": { + "name": "auth_session_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.auth_user": { + "name": "auth_user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "auth_user_email_unique": { + "name": "auth_user_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.auth_verification": { + "name": "auth_verification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "auth_verification_identifier_idx": { + "name": "auth_verification_identifier_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.authorization_audit_events": { + "name": "authorization_audit_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "team_public_id": { + "name": "team_public_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actor_account_id": { + "name": "actor_account_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "target_member_id": { + "name": "target_member_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "target_email": { + "name": "target_email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "before_permissions": { + "name": "before_permissions", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "after_permissions": { + "name": "after_permissions", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "authorization_audit_team_created_idx": { + "name": "authorization_audit_team_created_idx", + "columns": [ + { + "expression": "team_public_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "authorization_audit_expires_idx": { + "name": "authorization_audit_expires_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "authorization_audit_events_actor_account_id_accounts_id_fk": { + "name": "authorization_audit_events_actor_account_id_accounts_id_fk", + "tableFrom": "authorization_audit_events", + "tableTo": "accounts", + "columnsFrom": [ + "actor_account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.chatbot_settings": { + "name": "chatbot_settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'Assistant'" + }, + "greeting": { + "name": "greeting", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "accent_color": { + "name": "accent_color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "escalation_enabled": { + "name": "escalation_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "variant": { + "name": "variant", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'bubble'" + }, + "widget_style": { + "name": "widget_style", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "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": { + "chatbot_settings_team_id_teams_id_fk": { + "name": "chatbot_settings_team_id_teams_id_fk", + "tableFrom": "chatbot_settings", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "chatbot_settings_team_id_unique": { + "name": "chatbot_settings_team_id_unique", + "nullsNotDistinct": false, + "columns": [ + "team_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.custom_domains": { + "name": "custom_domains", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "hostname": { + "name": "hostname", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "primary": { + "name": "primary", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "routing_satisfied": { + "name": "routing_satisfied", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "verified_at": { + "name": "verified_at", + "type": "timestamp", + "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": { + "custom_domains_team_idx": { + "name": "custom_domains_team_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "custom_domains_team_id_teams_id_fk": { + "name": "custom_domains_team_id_teams_id_fk", + "tableFrom": "custom_domains", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "custom_domains_hostname_unique": { + "name": "custom_domains_hostname_unique", + "nullsNotDistinct": false, + "columns": [ + "hostname" + ] + } + }, + "policies": {}, + "checkConstraints": { + "custom_domains_verified_at_check": { + "name": "custom_domains_verified_at_check", + "value": "(\"status\" <> 'verified' OR \"verified_at\" IS NOT NULL)" + } + }, + "isRLSEnabled": false + }, + "public.doc_sections": { + "name": "doc_sections", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "section_id": { + "name": "section_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "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": { + "doc_sections_team_slug_idx": { + "name": "doc_sections_team_slug_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_sections_team_idx": { + "name": "doc_sections_team_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "doc_sections_team_id_teams_id_fk": { + "name": "doc_sections_team_id_teams_id_fk", + "tableFrom": "doc_sections", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "doc_sections_section_id_unique": { + "name": "doc_sections_section_id_unique", + "nullsNotDistinct": false, + "columns": [ + "section_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "section_id_prefix_check": { + "name": "section_id_prefix_check", + "value": "\"section_id\" LIKE 'sec\\_%' ESCAPE '\\'" + } + }, + "isRLSEnabled": false + }, + "public.document_chunks": { + "name": "document_chunks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "document_id": { + "name": "document_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "heading": { + "name": "heading", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "anchor": { + "name": "anchor", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "embedding": { + "name": "embedding", + "type": "vector(1536)", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "document_chunks_team_idx": { + "name": "document_chunks_team_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "document_chunks_embedding_idx": { + "name": "document_chunks_embedding_idx", + "columns": [ + { + "expression": "embedding", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "vector_cosine_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "hnsw", + "with": {} + } + }, + "foreignKeys": { + "document_chunks_team_id_teams_id_fk": { + "name": "document_chunks_team_id_teams_id_fk", + "tableFrom": "document_chunks", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "document_chunks_document_id_documents_id_fk": { + "name": "document_chunks_document_id_documents_id_fk", + "tableFrom": "document_chunks", + "tableTo": "documents", + "columnsFrom": [ + "document_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.document_index_jobs": { + "name": "document_index_jobs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "document_id": { + "name": "document_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'queued'" + }, + "generation": { + "name": "generation", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "lease_expires_at": { + "name": "lease_expires_at", + "type": "timestamp", + "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()" + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "finished_at": { + "name": "finished_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "document_index_jobs_document_idx": { + "name": "document_index_jobs_document_idx", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "document_index_jobs_poll_idx": { + "name": "document_index_jobs_poll_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "lease_expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "document_index_jobs_team_id_teams_id_fk": { + "name": "document_index_jobs_team_id_teams_id_fk", + "tableFrom": "document_index_jobs", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "document_index_jobs_document_id_documents_id_fk": { + "name": "document_index_jobs_document_id_documents_id_fk", + "tableFrom": "document_index_jobs", + "tableTo": "documents", + "columnsFrom": [ + "document_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.documentation_api_operations": { + "name": "documentation_api_operations", + "schema": "", + "columns": { + "spec_id": { + "name": "spec_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "route": { + "name": "route", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "group": { + "name": "group", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "method": { + "name": "method", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "deprecated": { + "name": "deprecated", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "slice": { + "name": "slice", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chunks": { + "name": "chunks", + "type": "jsonb", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "documentation_api_operations_route_idx": { + "name": "documentation_api_operations_route_idx", + "columns": [ + { + "expression": "spec_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "route", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "documentation_api_operations_team_idx": { + "name": "documentation_api_operations_team_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "documentation_api_operations_spec_id_documentation_api_specs_spec_id_fk": { + "name": "documentation_api_operations_spec_id_documentation_api_specs_spec_id_fk", + "tableFrom": "documentation_api_operations", + "tableTo": "documentation_api_specs", + "columnsFrom": [ + "spec_id" + ], + "columnsTo": [ + "spec_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "documentation_api_operations_team_id_teams_id_fk": { + "name": "documentation_api_operations_team_id_teams_id_fk", + "tableFrom": "documentation_api_operations", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.documentation_api_specs": { + "name": "documentation_api_specs", + "schema": "", + "columns": { + "spec_id": { + "name": "spec_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "commit_sha": { + "name": "commit_sha", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_path": { + "name": "source_path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_blob_sha": { + "name": "source_blob_sha", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mount": { + "name": "mount", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "spec": { + "name": "spec", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "spec_hash": { + "name": "spec_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "info": { + "name": "info", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "diagnostics": { + "name": "diagnostics", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "documentation_api_specs_team_idx": { + "name": "documentation_api_specs_team_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "documentation_api_specs_team_id_teams_id_fk": { + "name": "documentation_api_specs_team_id_teams_id_fk", + "tableFrom": "documentation_api_specs", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.documentation_delivery_outbox": { + "name": "documentation_delivery_outbox", + "schema": "", + "columns": { + "event_id": { + "name": "event_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "publication_id": { + "name": "publication_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "policy_epoch": { + "name": "policy_epoch", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "destinations": { + "name": "destinations", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[\"local\"]'::jsonb" + }, + "acknowledgments": { + "name": "acknowledgments", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "next_attempt_at": { + "name": "next_attempt_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "lease_token": { + "name": "lease_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "lease_expires_at": { + "name": "lease_expires_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": {}, + "foreignKeys": { + "documentation_delivery_outbox_team_id_teams_id_fk": { + "name": "documentation_delivery_outbox_team_id_teams_id_fk", + "tableFrom": "documentation_delivery_outbox", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.documentation_embedding_configs": { + "name": "documentation_embedding_configs", + "schema": "", + "columns": { + "config_hash": { + "name": "config_hash", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "deployment": { + "name": "deployment", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "endpoint": { + "name": "endpoint", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_api_key": { + "name": "encrypted_api_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "documentation_embedding_configs_team_id_teams_id_fk": { + "name": "documentation_embedding_configs_team_id_teams_id_fk", + "tableFrom": "documentation_embedding_configs", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.documentation_embeddings": { + "name": "documentation_embeddings", + "schema": "", + "columns": { + "revision_id": { + "name": "revision_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config_hash": { + "name": "config_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "vectors": { + "name": "vectors", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "documentation_embeddings_revision_config_idx": { + "name": "documentation_embeddings_revision_config_idx", + "columns": [ + { + "expression": "revision_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "config_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "documentation_embeddings_revision_id_documentation_revisions_revision_id_fk": { + "name": "documentation_embeddings_revision_id_documentation_revisions_revision_id_fk", + "tableFrom": "documentation_embeddings", + "tableTo": "documentation_revisions", + "columnsFrom": [ + "revision_id" + ], + "columnsTo": [ + "revision_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.documentation_evidence": { + "name": "documentation_evidence", + "schema": "", + "columns": { + "evidence_id": { + "name": "evidence_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "item_id": { + "name": "item_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ticket_id": { + "name": "ticket_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "message_id": { + "name": "message_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "dedupe_key": { + "name": "dedupe_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "trigger": { + "name": "trigger", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "retrieved": { + "name": "retrieved", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "documentation_evidence_dedupe_idx": { + "name": "documentation_evidence_dedupe_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "dedupe_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "documentation_evidence_team_id_teams_id_fk": { + "name": "documentation_evidence_team_id_teams_id_fk", + "tableFrom": "documentation_evidence", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "documentation_evidence_item_id_documentation_improvements_item_id_fk": { + "name": "documentation_evidence_item_id_documentation_improvements_item_id_fk", + "tableFrom": "documentation_evidence", + "tableTo": "documentation_improvements", + "columnsFrom": [ + "item_id" + ], + "columnsTo": [ + "item_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "documentation_evidence_ticket_id_tickets_id_fk": { + "name": "documentation_evidence_ticket_id_tickets_id_fk", + "tableFrom": "documentation_evidence", + "tableTo": "tickets", + "columnsFrom": [ + "ticket_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "documentation_evidence_message_id_ticket_messages_id_fk": { + "name": "documentation_evidence_message_id_ticket_messages_id_fk", + "tableFrom": "documentation_evidence", + "tableTo": "ticket_messages", + "columnsFrom": [ + "message_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.documentation_git_bindings": { + "name": "documentation_git_bindings", + "schema": "", + "columns": { + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "installation_id": { + "name": "installation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "repository_id": { + "name": "repository_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owner_login": { + "name": "owner_login", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "repository_name": { + "name": "repository_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "repository_private": { + "name": "repository_private", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "branch": { + "name": "branch", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "directory": { + "name": "directory", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "docs_enabled": { + "name": "docs_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "pages_directory": { + "name": "pages_directory", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pages_source_key": { + "name": "pages_source_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "docs_source_key": { + "name": "docs_source_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pages_diagnostics": { + "name": "pages_diagnostics", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "api_spec_path": { + "name": "api_spec_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "api_mount": { + "name": "api_mount", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'api-reference'" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "generation": { + "name": "generation", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "requested_generation": { + "name": "requested_generation", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "grant_version": { + "name": "grant_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "grant_actor_account_id": { + "name": "grant_actor_account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "grant_github_user_id": { + "name": "grant_github_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "repository_admin_verified": { + "name": "repository_admin_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "capabilities": { + "name": "capabilities", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "granted_at": { + "name": "granted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "observed_sha": { + "name": "observed_sha", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "validated_sha": { + "name": "validated_sha", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "live_sha": { + "name": "live_sha", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_successful_sync_at": { + "name": "last_successful_sync_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_diagnostic": { + "name": "last_diagnostic", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "connection_receipt": { + "name": "connection_receipt", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "next_reconcile_at": { + "name": "next_reconcile_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "next_pr_reconcile_at": { + "name": "next_pr_reconcile_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_pr_reconcile_error": { + "name": "last_pr_reconcile_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "documentation_git_bindings_installation_idx": { + "name": "documentation_git_bindings_installation_idx", + "columns": [ + { + "expression": "installation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "documentation_git_bindings_reconcile_idx": { + "name": "documentation_git_bindings_reconcile_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "next_reconcile_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "documentation_git_bindings_pr_reconcile_idx": { + "name": "documentation_git_bindings_pr_reconcile_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "next_pr_reconcile_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "documentation_git_bindings_team_id_teams_id_fk": { + "name": "documentation_git_bindings_team_id_teams_id_fk", + "tableFrom": "documentation_git_bindings", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.documentation_git_checks": { + "name": "documentation_git_checks", + "schema": "", + "columns": { + "check_id": { + "name": "check_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "connection_generation": { + "name": "connection_generation", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "pull_request_number": { + "name": "pull_request_number", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "sync_run_id": { + "name": "sync_run_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "publication_result": { + "name": "publication_result", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'queued'" + }, + "result": { + "name": "result", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "check_run_id": { + "name": "check_run_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_phase": { + "name": "error_phase", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "lease_token": { + "name": "lease_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "lease_expires_at": { + "name": "lease_expires_at", + "type": "timestamp", + "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()" + }, + "finished_at": { + "name": "finished_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "documentation_git_checks_poll_idx": { + "name": "documentation_git_checks_poll_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "next_attempt_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "documentation_git_checks_sync_run_idx": { + "name": "documentation_git_checks_sync_run_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sync_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "documentation_git_checks_active_pr_idx": { + "name": "documentation_git_checks_active_pr_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "pull_request_number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"documentation_git_checks\".\"status\" in ('queued', 'validating', 'reporting')", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "documentation_git_checks_team_id_teams_id_fk": { + "name": "documentation_git_checks_team_id_teams_id_fk", + "tableFrom": "documentation_git_checks", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.documentation_git_deliveries": { + "name": "documentation_git_deliveries", + "schema": "", + "columns": { + "delivery_id": { + "name": "delivery_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "event": { + "name": "event", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "outcome": { + "name": "outcome", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'ignored'" + }, + "run_ids": { + "name": "run_ids", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "installation_id": { + "name": "installation_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "repository_id": { + "name": "repository_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "received_at": { + "name": "received_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.documentation_git_files": { + "name": "documentation_git_files", + "schema": "", + "columns": { + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_path": { + "name": "source_path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "document_id": { + "name": "document_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "commit_sha": { + "name": "commit_sha", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "blob_sha": { + "name": "blob_sha", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_hash": { + "name": "source_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "markdown": { + "name": "markdown", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "frontmatter": { + "name": "frontmatter", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "snapshot": { + "name": "snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "removed_at": { + "name": "removed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "documentation_git_files_path_idx": { + "name": "documentation_git_files_path_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_path", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "documentation_git_files_document_idx": { + "name": "documentation_git_files_document_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"documentation_git_files\".\"removed_at\" is null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "documentation_git_files_team_id_teams_id_fk": { + "name": "documentation_git_files_team_id_teams_id_fk", + "tableFrom": "documentation_git_files", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.documentation_git_grants": { + "name": "documentation_git_grants", + "schema": "", + "columns": { + "grant_id": { + "name": "grant_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "input_hash": { + "name": "input_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actor_account_id": { + "name": "actor_account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "github_user_id": { + "name": "github_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "installation_id": { + "name": "installation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "repository_id": { + "name": "repository_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "branch": { + "name": "branch", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "directory": { + "name": "directory", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "grant_version": { + "name": "grant_version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "connection_generation": { + "name": "connection_generation", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "capabilities": { + "name": "capabilities", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "repository_admin_verified": { + "name": "repository_admin_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "granted_at": { + "name": "granted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "documentation_git_grants_idempotency_idx": { + "name": "documentation_git_grants_idempotency_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "documentation_git_grants_version_idx": { + "name": "documentation_git_grants_version_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "grant_version", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "documentation_git_grants_team_id_teams_id_fk": { + "name": "documentation_git_grants_team_id_teams_id_fk", + "tableFrom": "documentation_git_grants", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.documentation_git_jobs": { + "name": "documentation_git_jobs", + "schema": "", + "columns": { + "run_id": { + "name": "run_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "connection_generation": { + "name": "connection_generation", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "request_generation": { + "name": "request_generation", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "trigger": { + "name": "trigger", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'reconcile'" + }, + "reviewed_sha": { + "name": "reviewed_sha", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "observed_sha": { + "name": "observed_sha", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "publication_id": { + "name": "publication_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'queued'" + }, + "diagnostic": { + "name": "diagnostic", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "diagnostic_report": { + "name": "diagnostic_report", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "next_attempt_at": { + "name": "next_attempt_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "lease_token": { + "name": "lease_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "lease_expires_at": { + "name": "lease_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "finished_at": { + "name": "finished_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "documentation_git_jobs_generation_idx": { + "name": "documentation_git_jobs_generation_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "connection_generation", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "request_generation", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "documentation_git_jobs_poll_idx": { + "name": "documentation_git_jobs_poll_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "next_attempt_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "documentation_git_jobs_team_id_teams_id_fk": { + "name": "documentation_git_jobs_team_id_teams_id_fk", + "tableFrom": "documentation_git_jobs", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.documentation_git_migration_prs": { + "name": "documentation_git_migration_prs", + "schema": "", + "columns": { + "plan_id": { + "name": "plan_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "setup_id": { + "name": "setup_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actor_credential": { + "name": "actor_credential", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "input_hash": { + "name": "input_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "policy_epoch": { + "name": "policy_epoch", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "purge_generation": { + "name": "purge_generation", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "initialize_empty_repository": { + "name": "initialize_empty_repository", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'queued'" + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "branch": { + "name": "branch", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "base_sha": { + "name": "base_sha", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tree_sha": { + "name": "tree_sha", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "commit_sha": { + "name": "commit_sha", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bootstrap_sha": { + "name": "bootstrap_sha", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pull_request_number": { + "name": "pull_request_number", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "pull_request_url": { + "name": "pull_request_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "merged_commit_sha": { + "name": "merged_commit_sha", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "diagnostic": { + "name": "diagnostic", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "next_attempt_at": { + "name": "next_attempt_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "lease_token": { + "name": "lease_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "lease_expires_at": { + "name": "lease_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "finished_at": { + "name": "finished_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "documentation_git_migration_prs_idempotency_idx": { + "name": "documentation_git_migration_prs_idempotency_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "documentation_git_migration_prs_work_idx": { + "name": "documentation_git_migration_prs_work_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "next_attempt_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "documentation_git_migration_prs_plan_id_documentation_git_migrations_plan_id_fk": { + "name": "documentation_git_migration_prs_plan_id_documentation_git_migrations_plan_id_fk", + "tableFrom": "documentation_git_migration_prs", + "tableTo": "documentation_git_migrations", + "columnsFrom": [ + "plan_id" + ], + "columnsTo": [ + "plan_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "documentation_git_migration_prs_team_id_teams_id_fk": { + "name": "documentation_git_migration_prs_team_id_teams_id_fk", + "tableFrom": "documentation_git_migration_prs", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "documentation_git_migration_prs_setup_id_documentation_git_setups_setup_id_fk": { + "name": "documentation_git_migration_prs_setup_id_documentation_git_setups_setup_id_fk", + "tableFrom": "documentation_git_migration_prs", + "tableTo": "documentation_git_setups", + "columnsFrom": [ + "setup_id" + ], + "columnsTo": [ + "setup_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.documentation_git_migrations": { + "name": "documentation_git_migrations", + "schema": "", + "columns": { + "plan_id": { + "name": "plan_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "setup_id": { + "name": "setup_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actor_account_id": { + "name": "actor_account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "preflight_id": { + "name": "preflight_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "docs_draft_version": { + "name": "docs_draft_version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "active_publication_id": { + "name": "active_publication_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "plan_hash": { + "name": "plan_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "selection": { + "name": "selection", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "files": { + "name": "files", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "sections": { + "name": "sections", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'planned'" + }, + "verified_sha": { + "name": "verified_sha", + "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()" + }, + "activated_at": { + "name": "activated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "documentation_git_migrations_team_idx": { + "name": "documentation_git_migrations_team_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "documentation_git_migrations_team_id_teams_id_fk": { + "name": "documentation_git_migrations_team_id_teams_id_fk", + "tableFrom": "documentation_git_migrations", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "documentation_git_migrations_setup_id_documentation_git_setups_setup_id_fk": { + "name": "documentation_git_migrations_setup_id_documentation_git_setups_setup_id_fk", + "tableFrom": "documentation_git_migrations", + "tableTo": "documentation_git_setups", + "columnsFrom": [ + "setup_id" + ], + "columnsTo": [ + "setup_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "documentation_git_migrations_preflight_id_documentation_git_preflights_preflight_id_fk": { + "name": "documentation_git_migrations_preflight_id_documentation_git_preflights_preflight_id_fk", + "tableFrom": "documentation_git_migrations", + "tableTo": "documentation_git_preflights", + "columnsFrom": [ + "preflight_id" + ], + "columnsTo": [ + "preflight_id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.documentation_git_preflights": { + "name": "documentation_git_preflights", + "schema": "", + "columns": { + "preflight_id": { + "name": "preflight_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "setup_id": { + "name": "setup_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "installation_id": { + "name": "installation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "repository_id": { + "name": "repository_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owner_login": { + "name": "owner_login", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "repository_name": { + "name": "repository_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "repository_private": { + "name": "repository_private", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "branch": { + "name": "branch", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "directory": { + "name": "directory", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "commit_sha": { + "name": "commit_sha", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "docs_draft_version": { + "name": "docs_draft_version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "manifest_hash": { + "name": "manifest_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "manifest": { + "name": "manifest", + "type": "jsonb", + "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": { + "documentation_git_preflights_team_idx": { + "name": "documentation_git_preflights_team_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "documentation_git_preflights_team_id_teams_id_fk": { + "name": "documentation_git_preflights_team_id_teams_id_fk", + "tableFrom": "documentation_git_preflights", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "documentation_git_preflights_setup_id_documentation_git_setups_setup_id_fk": { + "name": "documentation_git_preflights_setup_id_documentation_git_setups_setup_id_fk", + "tableFrom": "documentation_git_preflights", + "tableTo": "documentation_git_setups", + "columnsFrom": [ + "setup_id" + ], + "columnsTo": [ + "setup_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.documentation_git_proposal_documents": { + "name": "documentation_git_proposal_documents", + "schema": "", + "columns": { + "proposal_id": { + "name": "proposal_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "document_id": { + "name": "document_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "draft_version": { + "name": "draft_version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "source_path": { + "name": "source_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "target_path": { + "name": "target_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "open": { + "name": "open", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + } + }, + "indexes": { + "documentation_git_proposal_documents_identity_idx": { + "name": "documentation_git_proposal_documents_identity_idx", + "columns": [ + { + "expression": "proposal_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "documentation_git_proposal_documents_open_idx": { + "name": "documentation_git_proposal_documents_open_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"documentation_git_proposal_documents\".\"open\" = true", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "documentation_git_proposal_documents_proposal_id_documentation_git_proposals_proposal_id_fk": { + "name": "documentation_git_proposal_documents_proposal_id_documentation_git_proposals_proposal_id_fk", + "tableFrom": "documentation_git_proposal_documents", + "tableTo": "documentation_git_proposals", + "columnsFrom": [ + "proposal_id" + ], + "columnsTo": [ + "proposal_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "documentation_git_proposal_documents_team_id_teams_id_fk": { + "name": "documentation_git_proposal_documents_team_id_teams_id_fk", + "tableFrom": "documentation_git_proposal_documents", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.documentation_git_proposals": { + "name": "documentation_git_proposals", + "schema": "", + "columns": { + "proposal_id": { + "name": "proposal_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "input_hash": { + "name": "input_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actor_credential": { + "name": "actor_credential", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "source_context": { + "name": "source_context", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "connection_generation": { + "name": "connection_generation", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "grant_version": { + "name": "grant_version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "mode": { + "name": "mode", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updates_proposal_id": { + "name": "updates_proposal_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "changes": { + "name": "changes", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "base_sha": { + "name": "base_sha", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tree_sha": { + "name": "tree_sha", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "commit_sha": { + "name": "commit_sha", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "merged_commit_sha": { + "name": "merged_commit_sha", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "branch": { + "name": "branch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pull_request_number": { + "name": "pull_request_number", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "pull_request_url": { + "name": "pull_request_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'queued'" + }, + "diagnostics": { + "name": "diagnostics", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "lease_token": { + "name": "lease_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "lease_expires_at": { + "name": "lease_expires_at", + "type": "timestamp", + "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()" + }, + "finished_at": { + "name": "finished_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "documentation_git_proposals_idempotency_idx": { + "name": "documentation_git_proposals_idempotency_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "documentation_git_proposals_poll_idx": { + "name": "documentation_git_proposals_poll_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "next_attempt_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "documentation_git_proposals_team_id_teams_id_fk": { + "name": "documentation_git_proposals_team_id_teams_id_fk", + "tableFrom": "documentation_git_proposals", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.documentation_git_setups": { + "name": "documentation_git_setups", + "schema": "", + "columns": { + "setup_id": { + "name": "setup_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actor_account_id": { + "name": "actor_account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state_hash": { + "name": "state_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "encrypted_verifier": { + "name": "encrypted_verifier", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_user_token": { + "name": "encrypted_user_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "github_user_id": { + "name": "github_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "github_login": { + "name": "github_login", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "purpose": { + "name": "purpose", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "return_path": { + "name": "return_path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "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": { + "documentation_git_setups_expiry_idx": { + "name": "documentation_git_setups_expiry_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "documentation_git_setups_team_id_teams_id_fk": { + "name": "documentation_git_setups_team_id_teams_id_fk", + "tableFrom": "documentation_git_setups", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "documentation_git_setups_actor_account_id_accounts_id_fk": { + "name": "documentation_git_setups_actor_account_id_accounts_id_fk", + "tableFrom": "documentation_git_setups", + "tableTo": "accounts", + "columnsFrom": [ + "actor_account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "documentation_git_setups_state_hash_unique": { + "name": "documentation_git_setups_state_hash_unique", + "nullsNotDistinct": false, + "columns": [ + "state_hash" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.documentation_improvement_drafts": { + "name": "documentation_improvement_drafts", + "schema": "", + "columns": { + "item_id": { + "name": "item_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "approved_version": { + "name": "approved_version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "item_version": { + "name": "item_version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "input_hash": { + "name": "input_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "document_id": { + "name": "document_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "draft_version": { + "name": "draft_version", + "type": "integer", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "documentation_improvement_drafts_version_idx": { + "name": "documentation_improvement_drafts_version_idx", + "columns": [ + { + "expression": "item_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "approved_version", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "documentation_improvement_drafts_item_id_documentation_improvements_item_id_fk": { + "name": "documentation_improvement_drafts_item_id_documentation_improvements_item_id_fk", + "tableFrom": "documentation_improvement_drafts", + "tableTo": "documentation_improvements", + "columnsFrom": [ + "item_id" + ], + "columnsTo": [ + "item_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.documentation_improvement_reviews": { + "name": "documentation_improvement_reviews", + "schema": "", + "columns": { + "review_id": { + "name": "review_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "item_id": { + "name": "item_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actor_account_id": { + "name": "actor_account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "item_version": { + "name": "item_version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "input": { + "name": "input", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "documentation_improvement_reviews_item_idx": { + "name": "documentation_improvement_reviews_item_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "item_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "documentation_improvement_reviews_team_id_teams_id_fk": { + "name": "documentation_improvement_reviews_team_id_teams_id_fk", + "tableFrom": "documentation_improvement_reviews", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "documentation_improvement_reviews_item_id_documentation_improvements_item_id_fk": { + "name": "documentation_improvement_reviews_item_id_documentation_improvements_item_id_fk", + "tableFrom": "documentation_improvement_reviews", + "tableTo": "documentation_improvements", + "columnsFrom": [ + "item_id" + ], + "columnsTo": [ + "item_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.documentation_improvements": { + "name": "documentation_improvements", + "schema": "", + "columns": { + "item_id": { + "name": "item_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'open'" + }, + "group_key": { + "name": "group_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_document_id": { + "name": "target_document_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "predecessor_id": { + "name": "predecessor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "merged_into_id": { + "name": "merged_into_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "approved": { + "name": "approved", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "draft_document_id": { + "name": "draft_document_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "draft_version": { + "name": "draft_version", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "draft_hash": { + "name": "draft_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "revision_id": { + "name": "revision_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "publication_id": { + "name": "publication_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": { + "documentation_improvements_team_idx": { + "name": "documentation_improvements_team_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "documentation_improvements_team_id_teams_id_fk": { + "name": "documentation_improvements_team_id_teams_id_fk", + "tableFrom": "documentation_improvements", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.documentation_memberships": { + "name": "documentation_memberships", + "schema": "", + "columns": { + "publication_id": { + "name": "publication_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "document_id": { + "name": "document_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "revision_id": { + "name": "revision_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "route": { + "name": "route", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "documentation_memberships_route_idx": { + "name": "documentation_memberships_route_idx", + "columns": [ + { + "expression": "publication_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "route", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "documentation_memberships_document_idx": { + "name": "documentation_memberships_document_idx", + "columns": [ + { + "expression": "publication_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "documentation_memberships_publication_id_documentation_publications_publication_id_fk": { + "name": "documentation_memberships_publication_id_documentation_publications_publication_id_fk", + "tableFrom": "documentation_memberships", + "tableTo": "documentation_publications", + "columnsFrom": [ + "publication_id" + ], + "columnsTo": [ + "publication_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "documentation_memberships_revision_id_documentation_revisions_revision_id_fk": { + "name": "documentation_memberships_revision_id_documentation_revisions_revision_id_fk", + "tableFrom": "documentation_memberships", + "tableTo": "documentation_revisions", + "columnsFrom": [ + "revision_id" + ], + "columnsTo": [ + "revision_id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.documentation_publications": { + "name": "documentation_publications", + "schema": "", + "columns": { + "publication_id": { + "name": "publication_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "input_hash": { + "name": "input_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "base_publication_id": { + "name": "base_publication_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "admission_type": { + "name": "admission_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'explicit'" + }, + "git_context": { + "name": "git_context", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "generation": { + "name": "generation", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "purge_generation": { + "name": "purge_generation", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "docs_draft_version": { + "name": "docs_draft_version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "indexing": { + "name": "indexing", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "navigation": { + "name": "navigation", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'queued'" + }, + "diagnostics": { + "name": "diagnostics", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "lease_token": { + "name": "lease_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "lease_expires_at": { + "name": "lease_expires_at", + "type": "timestamp", + "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()" + }, + "activated_at": { + "name": "activated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "finished_at": { + "name": "finished_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "documentation_publications_idempotency_idx": { + "name": "documentation_publications_idempotency_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "documentation_publications_poll_idx": { + "name": "documentation_publications_poll_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "next_attempt_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "documentation_publications_team_id_teams_id_fk": { + "name": "documentation_publications_team_id_teams_id_fk", + "tableFrom": "documentation_publications", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.documentation_purges": { + "name": "documentation_purges", + "schema": "", + "columns": { + "purge_id": { + "name": "purge_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "plan_hash": { + "name": "plan_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "manifest": { + "name": "manifest", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "actor_account_id": { + "name": "actor_account_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'preflight'" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "documentation_purges_idempotency_idx": { + "name": "documentation_purges_idempotency_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "documentation_purges_team_id_teams_id_fk": { + "name": "documentation_purges_team_id_teams_id_fk", + "tableFrom": "documentation_purges", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.documentation_revisions": { + "name": "documentation_revisions", + "schema": "", + "columns": { + "revision_id": { + "name": "revision_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "document_id": { + "name": "document_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "hash": { + "name": "hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "schema_version": { + "name": "schema_version", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'docs-v1'" + }, + "snapshot": { + "name": "snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "markdown": { + "name": "markdown", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "headings": { + "name": "headings", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "chunks": { + "name": "chunks", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "documentation_revisions_team_idx": { + "name": "documentation_revisions_team_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "documentation_revisions_team_id_teams_id_fk": { + "name": "documentation_revisions_team_id_teams_id_fk", + "tableFrom": "documentation_revisions", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.documentation_settings": { + "name": "documentation_settings", + "schema": "", + "columns": { + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'editor'" + }, + "draft_version": { + "name": "draft_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "active_publication_id": { + "name": "active_publication_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "active_api_spec_id": { + "name": "active_api_spec_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "requested_generation": { + "name": "requested_generation", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "policy_epoch": { + "name": "policy_epoch", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "purge_generation": { + "name": "purge_generation", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "indexing": { + "name": "indexing", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{\"enabled\":true}'::jsonb" + } + }, + "indexes": {}, + "foreignKeys": { + "documentation_settings_team_id_teams_id_fk": { + "name": "documentation_settings_team_id_teams_id_fk", + "tableFrom": "documentation_settings", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.documents": { + "name": "documents", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "document_id": { + "name": "document_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "content": { + "name": "content", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "excerpt": { + "name": "excerpt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "featured_image": { + "name": "featured_image", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "meta": { + "name": "meta", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "published_at": { + "name": "published_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "draft_version": { + "name": "draft_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "draft_slug": { + "name": "draft_slug", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "draft_markdown": { + "name": "draft_markdown", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "draft_hash": { + "name": "draft_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "draft_git_source": { + "name": "draft_git_source", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "draft_title": { + "name": "draft_title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "draft_content": { + "name": "draft_content", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "draft_excerpt": { + "name": "draft_excerpt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "draft_featured_image": { + "name": "draft_featured_image", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "draft_meta": { + "name": "draft_meta", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "author_account_id": { + "name": "author_account_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": { + "documents_team_kind_slug_idx": { + "name": "documents_team_kind_slug_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"documents\".\"kind\" <> 'doc'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "documents_team_kind_idx": { + "name": "documents_team_kind_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "kind", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "documents_published_idx": { + "name": "documents_published_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "published_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "documents_team_id_teams_id_fk": { + "name": "documents_team_id_teams_id_fk", + "tableFrom": "documents", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "documents_author_account_id_accounts_id_fk": { + "name": "documents_author_account_id_accounts_id_fk", + "tableFrom": "documents", + "tableTo": "accounts", + "columnsFrom": [ + "author_account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "documents_document_id_unique": { + "name": "documents_document_id_unique", + "nullsNotDistinct": false, + "columns": [ + "document_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "document_id_prefix_check": { + "name": "document_id_prefix_check", + "value": "\"document_id\" LIKE 'doc\\_%' ESCAPE '\\'" + } + }, + "isRLSEnabled": false + }, + "public.email_reply_tokens": { + "name": "email_reply_tokens", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ticket_id": { + "name": "ticket_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "requester_email": { + "name": "requester_email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "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": { + "email_reply_tokens_ticket_requester_idx": { + "name": "email_reply_tokens_ticket_requester_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "ticket_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "requester_email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "email_reply_tokens_team_id_teams_id_fk": { + "name": "email_reply_tokens_team_id_teams_id_fk", + "tableFrom": "email_reply_tokens", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "email_reply_tokens_ticket_id_tickets_id_fk": { + "name": "email_reply_tokens_ticket_id_tickets_id_fk", + "tableFrom": "email_reply_tokens", + "tableTo": "tickets", + "columnsFrom": [ + "ticket_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "email_reply_tokens_token_unique": { + "name": "email_reply_tokens_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.git_assets": { + "name": "git_assets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "blob_sha": { + "name": "blob_sha", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "git_assets_team_blob_idx": { + "name": "git_assets_team_blob_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "blob_sha", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "git_assets_team_id_teams_id_fk": { + "name": "git_assets_team_id_teams_id_fk", + "tableFrom": "git_assets", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.inbound_emails": { + "name": "inbound_emails", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "message_id": { + "name": "message_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ticket_id": { + "name": "ticket_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "inbound_emails_team_id_teams_id_fk": { + "name": "inbound_emails_team_id_teams_id_fk", + "tableFrom": "inbound_emails", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "inbound_emails_ticket_id_tickets_id_fk": { + "name": "inbound_emails_ticket_id_tickets_id_fk", + "tableFrom": "inbound_emails", + "tableTo": "tickets", + "columnsFrom": [ + "ticket_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "inbound_emails_message_id_unique": { + "name": "inbound_emails_message_id_unique", + "nullsNotDistinct": false, + "columns": [ + "message_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.inbox_ai_runs": { + "name": "inbox_ai_runs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ticket_id": { + "name": "ticket_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "trigger_message_id": { + "name": "trigger_message_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "channel": { + "name": "channel", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'queued'" + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "skip_reason": { + "name": "skip_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stop_reason": { + "name": "stop_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "retrieval": { + "name": "retrieval", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "lease_expires_at": { + "name": "lease_expires_at", + "type": "timestamp", + "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()" + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "finished_at": { + "name": "finished_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "inbox_ai_runs_ticket_trigger_idx": { + "name": "inbox_ai_runs_ticket_trigger_idx", + "columns": [ + { + "expression": "ticket_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "trigger_message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "inbox_ai_runs_poll_idx": { + "name": "inbox_ai_runs_poll_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "lease_expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "inbox_ai_runs_team_id_teams_id_fk": { + "name": "inbox_ai_runs_team_id_teams_id_fk", + "tableFrom": "inbox_ai_runs", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "inbox_ai_runs_ticket_id_tickets_id_fk": { + "name": "inbox_ai_runs_ticket_id_tickets_id_fk", + "tableFrom": "inbox_ai_runs", + "tableTo": "tickets", + "columnsFrom": [ + "ticket_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "inbox_ai_runs_trigger_message_id_ticket_messages_id_fk": { + "name": "inbox_ai_runs_trigger_message_id_ticket_messages_id_fk", + "tableFrom": "inbox_ai_runs", + "tableTo": "ticket_messages", + "columnsFrom": [ + "trigger_message_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.inbox_ai_settings": { + "name": "inbox_ai_settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "channel": { + "name": "channel", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "auto_reply": { + "name": "auto_reply", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "escalate_on_visitor_request": { + "name": "escalate_on_visitor_request", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "escalate_when_unsure": { + "name": "escalate_when_unsure", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "escalate_topics": { + "name": "escalate_topics", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "escalate_after_bot_turns": { + "name": "escalate_after_bot_turns", + "type": "integer", + "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": { + "inbox_ai_settings_team_channel_idx": { + "name": "inbox_ai_settings_team_channel_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "channel", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "inbox_ai_settings_team_id_teams_id_fk": { + "name": "inbox_ai_settings_team_id_teams_id_fk", + "tableFrom": "inbox_ai_settings", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.integrations": { + "name": "integrations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "jsonb", + "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": { + "integrations_team_provider_idx": { + "name": "integrations_team_provider_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "integrations_team_id_teams_id_fk": { + "name": "integrations_team_id_teams_id_fk", + "tableFrom": "integrations", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.auth_jwks": { + "name": "auth_jwks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "alg": { + "name": "alg", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "crv": { + "name": "crv", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.media": { + "name": "media", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "media_id": { + "name": "media_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "media_lit_id": { + "name": "media_lit_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "thumbnail_url": { + "name": "thumbnail_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "file_name": { + "name": "file_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mime_type": { + "name": "mime_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "alt": { + "name": "alt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "caption": { + "name": "caption", + "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": { + "media_team_idx": { + "name": "media_team_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "media_medialit_idx": { + "name": "media_medialit_idx", + "columns": [ + { + "expression": "media_lit_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "media_team_id_teams_id_fk": { + "name": "media_team_id_teams_id_fk", + "tableFrom": "media", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "media_media_id_unique": { + "name": "media_media_id_unique", + "nullsNotDistinct": false, + "columns": [ + "media_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "media_id_prefix_check": { + "name": "media_id_prefix_check", + "value": "\"media_id\" LIKE 'med\\_%' ESCAPE '\\'" + } + }, + "isRLSEnabled": false + }, + "public.oauth_post_login_team_selections": { + "name": "oauth_post_login_team_selections", + "schema": "", + "columns": { + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "oauth_post_login_team_selections_session_id_auth_session_id_fk": { + "name": "oauth_post_login_team_selections_session_id_auth_session_id_fk", + "tableFrom": "oauth_post_login_team_selections", + "tableTo": "auth_session", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oauth_post_login_team_selections_team_id_teams_id_fk": { + "name": "oauth_post_login_team_selections_team_id_teams_id_fk", + "tableFrom": "oauth_post_login_team_selections", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.outbox_jobs": { + "name": "outbox_jobs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "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": "'pending'" + }, + "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()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "outbox_jobs_poll_idx": { + "name": "outbox_jobs_poll_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "next_attempt_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "outbox_jobs_team_id_teams_id_fk": { + "name": "outbox_jobs_team_id_teams_id_fk", + "tableFrom": "outbox_jobs", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.page_pull_requests": { + "name": "page_pull_requests", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "page_id": { + "name": "page_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "branch": { + "name": "branch", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "base_sha": { + "name": "base_sha", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "commit_sha": { + "name": "commit_sha", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "number": { + "name": "number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'open'" + }, + "directory": { + "name": "directory", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "files": { + "name": "files", + "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": { + "page_pull_requests_team_idx": { + "name": "page_pull_requests_team_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "page_pull_requests_team_id_teams_id_fk": { + "name": "page_pull_requests_team_id_teams_id_fk", + "tableFrom": "page_pull_requests", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "page_pull_requests_page_id_pages_id_fk": { + "name": "page_pull_requests_page_id_pages_id_fk", + "tableFrom": "page_pull_requests", + "tableTo": "pages", + "columnsFrom": [ + "page_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.page_revisions": { + "name": "page_revisions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "hash": { + "name": "hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "page_revisions_team_hash_idx": { + "name": "page_revisions_team_hash_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "page_revisions_team_id_teams_id_fk": { + "name": "page_revisions_team_id_teams_id_fk", + "tableFrom": "page_revisions", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.pages": { + "name": "pages", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "page_id": { + "name": "page_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "deletable": { + "name": "deletable", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "draft_layout": { + "name": "draft_layout", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "draft_title": { + "name": "draft_title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "draft_description": { + "name": "draft_description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "draft_social_image": { + "name": "draft_social_image", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "draft_robots_allowed": { + "name": "draft_robots_allowed", + "type": "boolean", + "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": { + "pages_team_idx": { + "name": "pages_team_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "pages_team_slug_idx": { + "name": "pages_team_slug_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "pages_team_id_teams_id_fk": { + "name": "pages_team_id_teams_id_fk", + "tableFrom": "pages", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "pages_page_id_unique": { + "name": "pages_page_id_unique", + "nullsNotDistinct": false, + "columns": [ + "page_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "page_id_prefix_check": { + "name": "page_id_prefix_check", + "value": "\"page_id\" LIKE 'pg\\_%' ESCAPE '\\'" + } + }, + "isRLSEnabled": false + }, + "public.rate_limit_buckets": { + "name": "rate_limit_buckets", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "count": { + "name": "count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "reset_at": { + "name": "reset_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.site_release_pages": { + "name": "site_release_pages", + "schema": "", + "columns": { + "release_id": { + "name": "release_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "page_id": { + "name": "page_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "revision_id": { + "name": "revision_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "site_release_pages_slug_idx": { + "name": "site_release_pages_slug_idx", + "columns": [ + { + "expression": "release_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "site_release_pages_page_idx": { + "name": "site_release_pages_page_idx", + "columns": [ + { + "expression": "page_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "site_release_pages_release_id_site_releases_id_fk": { + "name": "site_release_pages_release_id_site_releases_id_fk", + "tableFrom": "site_release_pages", + "tableTo": "site_releases", + "columnsFrom": [ + "release_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "site_release_pages_page_id_pages_id_fk": { + "name": "site_release_pages_page_id_pages_id_fk", + "tableFrom": "site_release_pages", + "tableTo": "pages", + "columnsFrom": [ + "page_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "site_release_pages_revision_id_page_revisions_id_fk": { + "name": "site_release_pages_revision_id_page_revisions_id_fk", + "tableFrom": "site_release_pages", + "tableTo": "page_revisions", + "columnsFrom": [ + "revision_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.site_releases": { + "name": "site_releases", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "release_id": { + "name": "release_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "commit_sha": { + "name": "commit_sha", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "docs_publication_id": { + "name": "docs_publication_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "site": { + "name": "site", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_key": { + "name": "source_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "site_releases_team_idx": { + "name": "site_releases_team_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "site_releases_team_id_teams_id_fk": { + "name": "site_releases_team_id_teams_id_fk", + "tableFrom": "site_releases", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "site_releases_release_id_unique": { + "name": "site_releases_release_id_unique", + "nullsNotDistinct": false, + "columns": [ + "release_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "release_id_prefix_check": { + "name": "release_id_prefix_check", + "value": "\"release_id\" LIKE 'rel\\_%' ESCAPE '\\'" + } + }, + "isRLSEnabled": false + }, + "public.team_invitations": { + "name": "team_invitations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "invitation_id": { + "name": "invitation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "permissions": { + "name": "permissions", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "invited_by_account_id": { + "name": "invited_by_account_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "accepted_at": { + "name": "accepted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "rejected_at": { + "name": "rejected_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp", + "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": { + "team_invitations_pending_team_email_idx": { + "name": "team_invitations_pending_team_email_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"team_invitations\".\"status\" = 'pending'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "team_invitations_team_created_idx": { + "name": "team_invitations_team_created_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "team_invitations_team_email_idx": { + "name": "team_invitations_team_email_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "team_invitations_expires_idx": { + "name": "team_invitations_expires_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "team_invitations_team_id_teams_id_fk": { + "name": "team_invitations_team_id_teams_id_fk", + "tableFrom": "team_invitations", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "team_invitations_invited_by_account_id_accounts_id_fk": { + "name": "team_invitations_invited_by_account_id_accounts_id_fk", + "tableFrom": "team_invitations", + "tableTo": "accounts", + "columnsFrom": [ + "invited_by_account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "team_invitations_invitation_id_unique": { + "name": "team_invitations_invitation_id_unique", + "nullsNotDistinct": false, + "columns": [ + "invitation_id" + ] + }, + "team_invitations_token_hash_unique": { + "name": "team_invitations_token_hash_unique", + "nullsNotDistinct": false, + "columns": [ + "token_hash" + ] + } + }, + "policies": {}, + "checkConstraints": { + "invitation_id_prefix_check": { + "name": "invitation_id_prefix_check", + "value": "\"invitation_id\" LIKE 'tinv\\_%' ESCAPE '\\'" + } + }, + "isRLSEnabled": false + }, + "public.team_members": { + "name": "team_members", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "member_id": { + "name": "member_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'member'" + }, + "permissions": { + "name": "permissions", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "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": { + "team_members_team_account_idx": { + "name": "team_members_team_account_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "team_members_team_id_teams_id_fk": { + "name": "team_members_team_id_teams_id_fk", + "tableFrom": "team_members", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "team_members_account_id_accounts_id_fk": { + "name": "team_members_account_id_accounts_id_fk", + "tableFrom": "team_members", + "tableTo": "accounts", + "columnsFrom": [ + "account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "team_members_member_id_unique": { + "name": "team_members_member_id_unique", + "nullsNotDistinct": false, + "columns": [ + "member_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "member_id_prefix_check": { + "name": "member_id_prefix_check", + "value": "\"member_id\" LIKE 'tmem\\_%' ESCAPE '\\'" + } + }, + "isRLSEnabled": false + }, + "public.team_settings": { + "name": "team_settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "subtitle": { + "name": "subtitle", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "logo": { + "name": "logo", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "theme_id": { + "name": "theme_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "code_injection_head": { + "name": "code_injection_head", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "code_injection_body": { + "name": "code_injection_body", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "draft_shared_widgets": { + "name": "draft_shared_widgets", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{\"header\":{\"widgetId\":\"shared-header\",\"name\":\"header\",\"deletable\":false,\"moveable\":false,\"shared\":true,\"settings\":{\"verticalPadding\":\"py-4\"}},\"footer\":{\"widgetId\":\"shared-footer\",\"name\":\"footer\",\"deletable\":false,\"moveable\":false,\"shared\":true,\"settings\":{}}}'::jsonb" + }, + "active_release_id": { + "name": "active_release_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": {}, + "foreignKeys": { + "team_settings_team_id_teams_id_fk": { + "name": "team_settings_team_id_teams_id_fk", + "tableFrom": "team_settings", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "team_settings_team_id_unique": { + "name": "team_settings_team_id_unique", + "nullsNotDistinct": false, + "columns": [ + "team_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.teams": { + "name": "teams", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owner_account_id": { + "name": "owner_account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "external_id": { + "name": "external_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "subdomain": { + "name": "subdomain", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "redirect_subdomain": { + "name": "redirect_subdomain", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "hide_from_search": { + "name": "hide_from_search", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "custom_domain_cname_target": { + "name": "custom_domain_cname_target", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "custom_domain_txt_record_name": { + "name": "custom_domain_txt_record_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "custom_domain_verification_token": { + "name": "custom_domain_verification_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "custom_domain_txt_satisfied": { + "name": "custom_domain_txt_satisfied", + "type": "boolean", + "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": {}, + "foreignKeys": { + "teams_owner_account_id_accounts_id_fk": { + "name": "teams_owner_account_id_accounts_id_fk", + "tableFrom": "teams", + "tableTo": "accounts", + "columnsFrom": [ + "owner_account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "teams_team_id_unique": { + "name": "teams_team_id_unique", + "nullsNotDistinct": false, + "columns": [ + "team_id" + ] + }, + "teams_external_id_unique": { + "name": "teams_external_id_unique", + "nullsNotDistinct": false, + "columns": [ + "external_id" + ] + }, + "teams_subdomain_unique": { + "name": "teams_subdomain_unique", + "nullsNotDistinct": false, + "columns": [ + "subdomain" + ] + } + }, + "policies": {}, + "checkConstraints": { + "team_id_prefix_check": { + "name": "team_id_prefix_check", + "value": "\"team_id\" LIKE 'team\\_%' ESCAPE '\\'" + } + }, + "isRLSEnabled": false + }, + "public.themes": { + "name": "themes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "theme_id": { + "name": "theme_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "style": { + "name": "style", + "type": "jsonb", + "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": { + "themes_team_idx": { + "name": "themes_team_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "themes_team_id_teams_id_fk": { + "name": "themes_team_id_teams_id_fk", + "tableFrom": "themes", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "themes_theme_id_unique": { + "name": "themes_theme_id_unique", + "nullsNotDistinct": false, + "columns": [ + "theme_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "theme_id_prefix_check": { + "name": "theme_id_prefix_check", + "value": "\"theme_id\" LIKE 'thm\\_%' ESCAPE '\\'" + } + }, + "isRLSEnabled": false + }, + "public.ticket_access_grants": { + "name": "ticket_access_grants", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ticket_id": { + "name": "ticket_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", + "primaryKey": false, + "notNull": true + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "ticket_access_grants_ticket_idx": { + "name": "ticket_access_grants_ticket_idx", + "columns": [ + { + "expression": "ticket_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ticket_access_grants_team_id_teams_id_fk": { + "name": "ticket_access_grants_team_id_teams_id_fk", + "tableFrom": "ticket_access_grants", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ticket_access_grants_ticket_id_tickets_id_fk": { + "name": "ticket_access_grants_ticket_id_tickets_id_fk", + "tableFrom": "ticket_access_grants", + "tableTo": "tickets", + "columnsFrom": [ + "ticket_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "ticket_access_grants_token_hash_unique": { + "name": "ticket_access_grants_token_hash_unique", + "nullsNotDistinct": false, + "columns": [ + "token_hash" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ticket_attachments": { + "name": "ticket_attachments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "attachment_id": { + "name": "attachment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ticket_id": { + "name": "ticket_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "message_id": { + "name": "message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "media_lit_id": { + "name": "media_lit_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "thumbnail_url": { + "name": "thumbnail_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "file_name": { + "name": "file_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "mime_type": { + "name": "mime_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "ticket_attachments_ticket_idx": { + "name": "ticket_attachments_ticket_idx", + "columns": [ + { + "expression": "ticket_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "ticket_attachments_message_idx": { + "name": "ticket_attachments_message_idx", + "columns": [ + { + "expression": "message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ticket_attachments_team_id_teams_id_fk": { + "name": "ticket_attachments_team_id_teams_id_fk", + "tableFrom": "ticket_attachments", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ticket_attachments_ticket_id_tickets_id_fk": { + "name": "ticket_attachments_ticket_id_tickets_id_fk", + "tableFrom": "ticket_attachments", + "tableTo": "tickets", + "columnsFrom": [ + "ticket_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ticket_attachments_message_id_ticket_messages_id_fk": { + "name": "ticket_attachments_message_id_ticket_messages_id_fk", + "tableFrom": "ticket_attachments", + "tableTo": "ticket_messages", + "columnsFrom": [ + "message_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "ticket_attachments_attachment_id_unique": { + "name": "ticket_attachments_attachment_id_unique", + "nullsNotDistinct": false, + "columns": [ + "attachment_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "attachment_id_prefix_check": { + "name": "attachment_id_prefix_check", + "value": "\"attachment_id\" LIKE 'att\\_%' ESCAPE '\\'" + } + }, + "isRLSEnabled": false + }, + "public.ticket_mail_outbox": { + "name": "ticket_mail_outbox", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ticket_id": { + "name": "ticket_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sequence": { + "name": "sequence", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "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()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "ticket_mail_outbox_ticket_seq_idx": { + "name": "ticket_mail_outbox_ticket_seq_idx", + "columns": [ + { + "expression": "ticket_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sequence", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "ticket_mail_outbox_poll_idx": { + "name": "ticket_mail_outbox_poll_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "next_attempt_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ticket_mail_outbox_team_id_teams_id_fk": { + "name": "ticket_mail_outbox_team_id_teams_id_fk", + "tableFrom": "ticket_mail_outbox", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ticket_mail_outbox_ticket_id_tickets_id_fk": { + "name": "ticket_mail_outbox_ticket_id_tickets_id_fk", + "tableFrom": "ticket_mail_outbox", + "tableTo": "tickets", + "columnsFrom": [ + "ticket_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ticket_messages": { + "name": "ticket_messages", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "message_id": { + "name": "message_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ticket_id": { + "name": "ticket_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "author": { + "name": "author", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "author_account_id": { + "name": "author_account_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "content": { + "name": "content", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "sources": { + "name": "sources", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "ticket_messages_ticket_idx": { + "name": "ticket_messages_ticket_idx", + "columns": [ + { + "expression": "ticket_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ticket_messages_team_id_teams_id_fk": { + "name": "ticket_messages_team_id_teams_id_fk", + "tableFrom": "ticket_messages", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ticket_messages_ticket_id_tickets_id_fk": { + "name": "ticket_messages_ticket_id_tickets_id_fk", + "tableFrom": "ticket_messages", + "tableTo": "tickets", + "columnsFrom": [ + "ticket_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ticket_messages_author_account_id_accounts_id_fk": { + "name": "ticket_messages_author_account_id_accounts_id_fk", + "tableFrom": "ticket_messages", + "tableTo": "accounts", + "columnsFrom": [ + "author_account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "ticket_messages_message_id_unique": { + "name": "ticket_messages_message_id_unique", + "nullsNotDistinct": false, + "columns": [ + "message_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "message_id_prefix_check": { + "name": "message_id_prefix_check", + "value": "\"message_id\" LIKE 'tmsg\\_%' ESCAPE '\\'" + } + }, + "isRLSEnabled": false + }, + "public.tickets": { + "name": "tickets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "ticket_id": { + "name": "ticket_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "subject": { + "name": "subject", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'open'" + }, + "requester_email": { + "name": "requester_email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "requester_name": { + "name": "requester_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'form'" + }, + "access_token_hash": { + "name": "access_token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "assignee_account_id": { + "name": "assignee_account_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ai_state": { + "name": "ai_state", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'idle'" + }, + "ai_generation": { + "name": "ai_generation", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "ai_trigger_message_id": { + "name": "ai_trigger_message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ai_stop_reason": { + "name": "ai_stop_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ai_stopped_at": { + "name": "ai_stopped_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_message_at": { + "name": "last_message_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "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": { + "tickets_team_status_idx": { + "name": "tickets_team_status_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_message_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "tickets_team_id_teams_id_fk": { + "name": "tickets_team_id_teams_id_fk", + "tableFrom": "tickets", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tickets_assignee_account_id_accounts_id_fk": { + "name": "tickets_assignee_account_id_accounts_id_fk", + "tableFrom": "tickets", + "tableTo": "accounts", + "columnsFrom": [ + "assignee_account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "tickets_ticket_id_unique": { + "name": "tickets_ticket_id_unique", + "nullsNotDistinct": false, + "columns": [ + "ticket_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "ticket_id_prefix_check": { + "name": "ticket_id_prefix_check", + "value": "\"ticket_id\" LIKE 'tkt\\_%' ESCAPE '\\'" + } + }, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/apps/api/drizzle/meta/_journal.json b/apps/api/drizzle/meta/_journal.json index 0977d3d..bbc043d 100644 --- a/apps/api/drizzle/meta/_journal.json +++ b/apps/api/drizzle/meta/_journal.json @@ -78,6 +78,13 @@ "when": 1792552069376, "tag": "0010_pages_from_git", "breakpoints": true + }, + { + "idx": 11, + "version": "7", + "when": 1792913948379, + "tag": "0011_openapi_reference", + "breakpoints": true } ] } \ No newline at end of file diff --git a/apps/api/package.json b/apps/api/package.json index 6906d9c..9f29a3c 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -68,6 +68,7 @@ "swagger-ui-express": "^5.0.1", "uuidv7": "^1.0.2", "ws": "^8.21.3", + "yaml": "^2.9.1", "zod": "^3.25.76", "zod-to-json-schema": "^3.25.2" }, diff --git a/apps/api/src/auth/operation-policy.ts b/apps/api/src/auth/operation-policy.ts index 9dad57f..7aba5b8 100644 --- a/apps/api/src/auth/operation-policy.ts +++ b/apps/api/src/auth/operation-policy.ts @@ -303,6 +303,21 @@ export const REST_OPERATION_POLICIES = { "unsupported", "deny", ), + "content.documentationGithub.getApiReference": teamPermission( + "documentation:read", + MCP_SCOPES.documentationRead, + "deny", + ), + "content.documentationGithub.previewApiReference": teamPermission( + "documentation:manage", + MCP_SCOPES.documentationManage, + "deny", + ), + "content.documentationGithub.updateApiReference": teamPermission( + "documentation:manage", + "unsupported", + "deny", + ), "content.documentationGithub.setup": teamPermission( "documentation:manage", MCP_SCOPES.documentationManage, diff --git a/apps/api/src/chatbot/completion.ts b/apps/api/src/chatbot/completion.ts index 05d0ad8..59b4b44 100644 --- a/apps/api/src/chatbot/completion.ts +++ b/apps/api/src/chatbot/completion.ts @@ -8,7 +8,7 @@ export interface ChatSource { slug: string; title: string | null; anchor: string | null; - kind?: "blog" | "doc" | "changelog"; + kind?: "blog" | "doc" | "changelog" | "api-operation"; revisionId?: string; publicationId?: string | null; policyEpoch?: number; diff --git a/apps/api/src/db/schema.ts b/apps/api/src/db/schema.ts index a60f5ad..1f427a0 100644 --- a/apps/api/src/db/schema.ts +++ b/apps/api/src/db/schema.ts @@ -932,7 +932,7 @@ export const ticketMessageAuthors = ["requester", "agent", "bot"] as const; export type TicketMessageAuthor = (typeof ticketMessageAuthors)[number]; export type TicketMessageSource = { - kind?: "blog" | "doc" | "changelog"; + kind?: "blog" | "doc" | "changelog" | "api-operation"; revisionId?: string; publicationId?: string | null; policyEpoch?: number; @@ -1058,7 +1058,7 @@ export type InboxAiRetrievalHit = { heading: string | null; anchor: string | null; similarity: number; - kind?: "blog" | "doc" | "changelog"; + kind?: "blog" | "doc" | "changelog" | "api-operation"; }; export const inboxAiRuns = pgTable( @@ -1292,6 +1292,8 @@ export const documentationSettings = pgTable("documentation_settings", { .default("editor"), draftVersion: integer("draft_version").notNull().default(1), activePublicationId: text("active_publication_id"), + /** The live API reference (docs/openapi-support.md); independent of docs. */ + activeApiSpecId: text("active_api_spec_id"), requestedGeneration: integer("requested_generation").notNull().default(0), policyEpoch: integer("policy_epoch").notNull().default(0), purgeGeneration: integer("purge_generation").notNull().default(0), @@ -1434,7 +1436,7 @@ export const documentationDeliveryOutbox = pgTable("documentation_delivery_outbo publicationId: text("publication_id"), policyEpoch: integer("policy_epoch").notNull(), type: text("type", { - enum: ["activation", "policy", "purge"], + enum: ["activation", "policy", "purge", "api_reference"], }).notNull(), payload: jsonb("payload").$type>().notNull().default({}), destinations: jsonb("destinations").$type().notNull().default(["local"]), @@ -1446,6 +1448,64 @@ export const documentationDeliveryOutbox = pgTable("documentation_delivery_outbo completedAt: timestamp("completed_at"), createdAt: timestamp("created_at").notNull().defaultNow(), }); + +/** API reference imported from a team's OpenAPI spec in Git + * (docs/openapi-support.md). Only the live spec and the latest failed + * attempt are kept. Specs and slices are `text` because `jsonb` reorders + * object keys. */ +export const documentationApiSpecs = pgTable( + "documentation_api_specs", + { + specId: text("spec_id") + .primaryKey() + .$defaultFn(() => genPublicId("aspec")), + teamId: text("team_id") + .notNull() + .references(() => teams.id, { onDelete: "cascade" }), + status: text("status", { enum: ["active", "failed"] }).notNull(), + commitSha: text("commit_sha").notNull(), + sourcePath: text("source_path").notNull(), + sourceBlobSha: text("source_blob_sha"), + mount: text("mount").notNull(), + /** The served `/openapi.json`; null for a failed attempt. */ + spec: text("spec"), + specHash: text("spec_hash"), + info: jsonb("info").$type>(), + diagnostics: jsonb("diagnostics") + .$type[]>() + .notNull() + .default([]), + createdAt: timestamp("created_at").notNull().defaultNow(), + }, + (t) => [index("documentation_api_specs_team_idx").on(t.teamId, t.status)], +); +export const documentationApiOperations = pgTable( + "documentation_api_operations", + { + specId: text("spec_id") + .notNull() + .references(() => documentationApiSpecs.specId, { onDelete: "cascade" }), + teamId: text("team_id") + .notNull() + .references(() => teams.id, { onDelete: "cascade" }), + /** Relative to `/docs`, like docs memberships: `api-reference/list-contacts`. */ + route: text("route").notNull(), + title: text("title").notNull(), + group: text("group").notNull(), + order: integer("order").notNull(), + method: text("method").notNull(), + path: text("path").notNull(), + deprecated: boolean("deprecated").notNull().default(false), + slice: text("slice").notNull(), + chunks: jsonb("chunks") + .$type<{ heading: string | null; anchor: string | null; content: string }[]>() + .notNull(), + }, + (t) => [ + uniqueIndex("documentation_api_operations_route_idx").on(t.specId, t.route), + index("documentation_api_operations_team_idx").on(t.teamId), + ], +); export const documentationPurges = pgTable( "documentation_purges", { @@ -1682,6 +1742,10 @@ export const documentationGitBindings = pgTable( docsSourceKey: text("docs_source_key"), /** Problems and warnings from the last pages import. */ pagesDiagnostics: jsonb("pages_diagnostics").$type[]>(), + /** Repo-relative path of the OpenAPI spec; null turns the API reference off. */ + apiSpecPath: text("api_spec_path"), + /** The API reference is served under `/docs/`. */ + apiMount: text("api_mount").notNull().default("api-reference"), status: text("status", { enum: ["connected", "paused", "access_lost", "disconnected"], }).notNull(), diff --git a/apps/api/src/documentation/authoring.ts b/apps/api/src/documentation/authoring.ts index ef0ab1f..4f9b037 100644 --- a/apps/api/src/documentation/authoring.ts +++ b/apps/api/src/documentation/authoring.ts @@ -8,6 +8,7 @@ import { documents, docSections, documentationSettings, + documentationGitBindings, documentationPublications, documentationGitFiles, media, @@ -240,6 +241,22 @@ export async function draftReport( ) ) add("DUPLICATE_ROUTE", "Another draft uses this route.", "slug"); + // The API reference owns /docs/ (docs/openapi-support.md §4). + const apiBinding = await ( + options.executor ?? db + ).query.documentationGitBindings.findFirst({ + where: eq(documentationGitBindings.teamId, teamId), + columns: { apiSpecPath: true, apiMount: true }, + }); + const apiMount = apiBinding?.apiSpecPath ? apiBinding.apiMount : null; + const underApiMount = (slug: string) => + !!apiMount && (slug === apiMount || slug.startsWith(`${apiMount}/`)); + if (route && underApiMount(route)) + add( + "RESERVED_ROUTE", + `/docs/${apiMount} is used by the API reference. Move this page or change its slug.`, + "slug", + ); const assets = await (options.executor ?? db).query.media.findMany({ where: eq(media.teamId, teamId), }); @@ -277,6 +294,8 @@ export async function draftReport( const target = new URL(href, base); if (!target.pathname.startsWith("/docs/")) continue; const slug = target.pathname.slice(6).replace(/\/$/, ""); + // Links to API reference pages aren't validated in v1. + if (underApiMount(slug)) continue; const doc = slug === route ? draft diff --git a/apps/api/src/documentation/github/errors.ts b/apps/api/src/documentation/github/errors.ts index b5c6d3d..2bcbfeb 100644 --- a/apps/api/src/documentation/github/errors.ts +++ b/apps/api/src/documentation/github/errors.ts @@ -6,6 +6,10 @@ import { import { GitHubDocsError } from "./app"; const messages: Record = { PAGES_ALREADY_SYNCED: "Pages already sync from this repository.", + API_SPEC_PATH_INVALID: + "Enter the spec's path in the repository, like apps/api/openapi.json. It must end in .json, .yaml or .yml.", + API_MOUNT_CONFLICT: + "That URL path is reserved or already used by docs pages. Choose another one.", GITHUB_NOTHING_TO_SYNC: "Choose docs, landing pages or both to sync from the repository.", PAGES_DIRECTORY_EXISTS: diff --git a/apps/api/src/documentation/github/routes.ts b/apps/api/src/documentation/github/routes.ts index a9ec397..f492801 100644 --- a/apps/api/src/documentation/github/routes.ts +++ b/apps/api/src/documentation/github/routes.ts @@ -22,6 +22,11 @@ import { previewGitPages, } from "../../pages/git-source"; import { createPagesSetupPullRequest } from "../../pages/git-export"; +import { + getApiReferenceStatus, + previewApiReference, + updateApiReferenceSettings, +} from "../openapi/service"; import { submitGitProposal, getGitProposal, listGitProposals } from "./proposals"; import { getGitCheck, listGitChecks } from "./check-reports"; import { resolveGitPublisherCredential } from "./publisher"; @@ -64,6 +69,12 @@ const implementation = s.router( run(() => enableGitPages(req.teamId, req.authz, body)), disablePages: async ({ req }: any) => run(() => disableGitPages(req.teamId, req.authz)), + getApiReference: async ({ req }: any) => + run(() => getApiReferenceStatus(req.teamId)), + previewApiReference: async ({ req, body }: any) => + run(() => previewApiReference(req.teamId, req.authz, body)), + updateApiReference: async ({ req, body }: any) => + run(() => updateApiReferenceSettings(req.teamId, req.authz, body)), exportPages: async ({ req }: any) => run(() => { requireGitManager(req.teamId, req.authz, true); @@ -153,6 +164,9 @@ const implementation = s.router( }, { previewPages: "content.documentationGithub.previewPages", + getApiReference: "content.documentationGithub.getApiReference", + previewApiReference: "content.documentationGithub.previewApiReference", + updateApiReference: "content.documentationGithub.updateApiReference", enablePages: "content.documentationGithub.enablePages", disablePages: "content.documentationGithub.disablePages", exportPages: "content.documentationGithub.exportPages", diff --git a/apps/api/src/documentation/github/worker.ts b/apps/api/src/documentation/github/worker.ts index d82ac92..e031bc6 100644 --- a/apps/api/src/documentation/github/worker.ts +++ b/apps/api/src/documentation/github/worker.ts @@ -24,6 +24,7 @@ import { import { lockSettings } from "../authoring"; import { captureRevision, digest } from "../publications"; import { captureRetrievalConfiguration } from "../retrieval"; +import { syncApiReference } from "../openapi/service"; import { DocsGitHubApp, getDocsGitHubAppConfig, GitHubDocsError } from "./app"; import { importDocumentationTree, @@ -590,6 +591,15 @@ export async function processGitSync(teamId?: string, injectedApp?: DocsGitHubAp sha, ); }; + // The API reference goes first: it never fails the sync, and the early + // returns below (docs off, commit already live) must not skip it. + await syncApiReference({ + teamId: job.teamId, + binding: binding!, + head, + readTree, + readBlob, + }); // A merged pages setup PR turns pages sync on in this same sync. const source = binding!.pagesDirectory ? binding! diff --git a/apps/api/src/documentation/improvements.ts b/apps/api/src/documentation/improvements.ts index 9ac3b15..bc7e2b7 100644 --- a/apps/api/src/documentation/improvements.ts +++ b/apps/api/src/documentation/improvements.ts @@ -138,7 +138,9 @@ export async function recordDocumentationEvidence(input: { messageId: message.id, dedupeKey, trigger: input.trigger, - retrieved: input.retrieved?.slice(0, 8) ?? [], + // Only docs pages are editable evidence; generated API reference + // pages (no revision) never become improvement targets. + retrieved: input.retrieved?.filter((r) => r.revisionId).slice(0, 8) ?? [], expiresAt: new Date(Date.now() + 30 * 86400000), }) .returning(); diff --git a/apps/api/src/documentation/openapi/fixtures/medialit.json b/apps/api/src/documentation/openapi/fixtures/medialit.json new file mode 100644 index 0000000..f5613de --- /dev/null +++ b/apps/api/src/documentation/openapi/fixtures/medialit.json @@ -0,0 +1,1170 @@ +{ + "openapi": "3.0.3", + "info": { + "title": "Medialit API", + "description": "Easy file uploads for serverless apps", + "version": "0.3.0", + "termsOfService": "https://medialit.cloud/p/terms", + "contact": { + "name": "Medialit Support", + "url": "https://medialit.cloud" + }, + "license": { + "name": "AGPL-3.0", + "url": "https://www.gnu.org/licenses/agpl-3.0.en.html" + } + }, + "servers": [ + { + "url": "{protocol}://{host}", + "description": "API Server", + "variables": { + "protocol": { + "default": "https", + "enum": ["https", "http"] + }, + "host": { + "default": "api.medialit.cloud" + } + } + } + ], + "tags": [ + { + "name": "Media", + "description": "Upload, list, and manage media resources." + }, + { + "name": "Settings", + "description": "Manage media processing configuration." + } + ], + "paths": { + "/health": { + "get": { + "summary": "Status of the server", + "description": "Returns the status of the server and uptime", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "ok" + }, + "uptime": { + "type": "number", + "example": 12.345 + } + } + } + } + } + } + }, + "operationId": "getHealth", + "security": [] + } + }, + "/settings/media/create": { + "post": { + "tags": ["Settings"], + "summary": "Update Media Settings", + "description": "Update configuration for media processing.", + "responses": { + "200": { + "description": "Settings updated successfully", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string", + "example": "success" + } + } + } + } + } + }, + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + }, + "example": { + "error": "Bad Request" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + }, + "example": { + "error": "Unauthorized" + } + } + } + }, + "403": { + "description": "Forbidden" + }, + "500": { + "description": "Internal Server Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + }, + "example": { + "error": "Internal Server Error" + } + } + } + } + }, + "security": [ + { + "bearerAuth": [] + }, + { + "apiKeyAuth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MediaSettingsPayload" + } + } + } + }, + "operationId": "updateMediaSettings" + } + }, + "/settings/media/get": { + "post": { + "tags": ["Settings"], + "summary": "Get Media Settings", + "description": "Retrieve current media processing configuration.", + "responses": { + "200": { + "description": "Settings retrieved successfully", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MediaSettings" + } + } + } + }, + "400": { + "description": "More than one credential was sent, for example both a Bearer token and an API key", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + }, + "example": { + "error": "credential_ambiguous", + "error_description": "Exactly one credential mechanism is allowed." + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + }, + "example": { + "error": "Unauthorized" + } + } + } + }, + "403": { + "description": "Forbidden" + }, + "500": { + "description": "Internal Server Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + }, + "example": { + "error": "Internal Server Error" + } + } + } + } + }, + "security": [ + { + "bearerAuth": [] + }, + { + "apiKeyAuth": [] + } + ], + "operationId": "getMediaSettings" + } + }, + "/media/signature/create": { + "post": { + "tags": ["Media"], + "summary": "Create Upload Signature", + "description": "Generate a signature for secure client-side uploads.", + "responses": { + "200": { + "description": "Signature generated successfully", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SignatureResponse" + } + } + } + }, + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + }, + "example": { + "error": "Bad Request" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + }, + "example": { + "error": "Unauthorized" + } + } + } + }, + "403": { + "description": "Forbidden" + }, + "500": { + "description": "Internal Server Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + }, + "example": { + "error": "Internal Server Error" + } + } + } + } + }, + "security": [ + { + "bearerAuth": [] + }, + { + "apiKeyAuth": [] + } + ], + "operationId": "createUploadSignature" + } + }, + "/media/create": { + "post": { + "tags": ["Media"], + "summary": "Upload Media", + "description": "Upload a new media file. Use OAuth Bearer auth, API key auth from Authorize (`x-medialit-apikey`), or pass `x-medialit-signature` for this endpoint only.", + "parameters": [ + { + "name": "x-medialit-signature", + "in": "header", + "description": "Upload Signature for secure client-side uploads", + "schema": { + "type": "string" + }, + "required": false + } + ], + "responses": { + "200": { + "description": "The created Media object", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Media" + } + } + } + }, + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + }, + "example": { + "error": "Bad Request" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + }, + "example": { + "error": "Unauthorized" + } + } + } + }, + "404": { + "description": "Invalid signature", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + }, + "example": { + "error": "Invalid signature" + } + } + } + }, + "500": { + "description": "Internal Server Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + }, + "example": { + "error": "Internal Server Error" + } + } + } + } + }, + "security": [ + { + "bearerAuth": [] + }, + { + "apiKeyAuth": [] + }, + { + "signatureAuth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "multipart/form-data": { + "schema": { + "type": "object", + "properties": { + "file": { + "type": "string", + "format": "binary" + }, + "caption": { + "type": "string" + }, + "access": { + "type": "string", + "enum": ["public", "private"] + }, + "group": { + "type": "string" + } + }, + "required": ["file"] + } + } + } + }, + "operationId": "uploadMedia" + } + }, + "/media/get/count": { + "post": { + "tags": ["Media"], + "summary": "Get Media Count", + "description": "Get the total number of media files.", + "responses": { + "200": { + "description": "Count retrieved successfully", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MediaCountResponse" + } + } + } + }, + "400": { + "description": "More than one credential was sent, for example both a Bearer token and an API key", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + }, + "example": { + "error": "credential_ambiguous", + "error_description": "Exactly one credential mechanism is allowed." + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + }, + "example": { + "error": "Unauthorized" + } + } + } + }, + "403": { + "description": "Forbidden" + }, + "500": { + "description": "Internal Server Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + }, + "example": { + "error": "Internal Server Error" + } + } + } + } + }, + "security": [ + { + "bearerAuth": [] + }, + { + "apiKeyAuth": [] + } + ], + "operationId": "getMediaCount" + } + }, + "/media/get/size": { + "post": { + "tags": ["Media"], + "summary": "Get Total Size", + "description": "Get the total size of all media files in bytes.", + "responses": { + "200": { + "description": "Size retrieved successfully", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MediaSizeResponse" + } + } + } + }, + "400": { + "description": "More than one credential was sent, for example both a Bearer token and an API key", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + }, + "example": { + "error": "credential_ambiguous", + "error_description": "Exactly one credential mechanism is allowed." + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + }, + "example": { + "error": "Unauthorized" + } + } + } + }, + "403": { + "description": "Forbidden" + }, + "500": { + "description": "Internal Server Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + }, + "example": { + "error": "Internal Server Error" + } + } + } + } + }, + "security": [ + { + "bearerAuth": [] + }, + { + "apiKeyAuth": [] + } + ], + "operationId": "getMediaSize" + } + }, + "/media/get/{mediaId}": { + "post": { + "tags": ["Media"], + "summary": "Get Media Details", + "description": "Retrieve metadata for a specific media item.", + "parameters": [ + { + "name": "mediaId", + "in": "path", + "required": true, + "description": "ID of the media file", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Media details retrieved successfully", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Media" + } + } + } + }, + "400": { + "description": "More than one credential was sent, for example both a Bearer token and an API key", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + }, + "example": { + "error": "credential_ambiguous", + "error_description": "Exactly one credential mechanism is allowed." + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + }, + "example": { + "error": "Unauthorized" + } + } + } + }, + "403": { + "description": "Forbidden" + }, + "404": { + "description": "Media not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + }, + "example": { + "error": "Not Found" + } + } + } + }, + "500": { + "description": "Internal Server Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + }, + "example": { + "error": "Internal Server Error" + } + } + } + } + }, + "security": [ + { + "bearerAuth": [] + }, + { + "apiKeyAuth": [] + } + ], + "operationId": "getMediaDetails" + } + }, + "/media/get": { + "post": { + "tags": ["Media"], + "summary": "List Media", + "description": "List media files filtered by optional query payload.", + "responses": { + "200": { + "description": "List retrieved successfully", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/MediaListItem" + } + } + } + } + }, + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + }, + "example": { + "error": "Bad Request" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + }, + "example": { + "error": "Unauthorized" + } + } + } + }, + "403": { + "description": "Forbidden" + }, + "500": { + "description": "Internal Server Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + }, + "example": { + "error": "Internal Server Error" + } + } + } + } + }, + "security": [ + { + "bearerAuth": [] + }, + { + "apiKeyAuth": [] + } + ], + "requestBody": { + "required": false, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetMediaQuery" + } + } + } + }, + "operationId": "listMedia" + } + }, + "/media/seal/{mediaId}": { + "post": { + "tags": ["Media"], + "summary": "Seal Media", + "description": "Seal a media file (mark as processed/finalized).", + "parameters": [ + { + "name": "mediaId", + "in": "path", + "required": true, + "description": "ID of the media file", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "The created Media object", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Media" + } + } + } + }, + "400": { + "description": "More than one credential was sent, for example both a Bearer token and an API key", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + }, + "example": { + "error": "credential_ambiguous", + "error_description": "Exactly one credential mechanism is allowed." + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + }, + "example": { + "error": "Unauthorized" + } + } + } + }, + "403": { + "description": "Forbidden" + }, + "404": { + "description": "Media not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + }, + "example": { + "error": "Not Found" + } + } + } + }, + "500": { + "description": "Internal Server Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + }, + "example": { + "error": "Internal Server Error" + } + } + } + } + }, + "security": [ + { + "bearerAuth": [] + }, + { + "apiKeyAuth": [] + } + ], + "operationId": "sealMedia" + } + }, + "/media/delete/{mediaId}": { + "delete": { + "tags": ["Media"], + "summary": "Delete Media", + "description": "Permanently delete a media file.", + "parameters": [ + { + "name": "mediaId", + "in": "path", + "required": true, + "description": "ID of the media file", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Media deleted successfully", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string", + "example": "success" + } + } + } + } + } + }, + "400": { + "description": "More than one credential was sent, for example both a Bearer token and an API key", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + }, + "example": { + "error": "credential_ambiguous", + "error_description": "Exactly one credential mechanism is allowed." + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + }, + "example": { + "error": "Unauthorized" + } + } + } + }, + "403": { + "description": "Forbidden" + }, + "500": { + "description": "Internal Server Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + }, + "example": { + "error": "Internal Server Error" + } + } + } + } + }, + "security": [ + { + "bearerAuth": [] + }, + { + "apiKeyAuth": [] + } + ], + "operationId": "deleteMedia" + } + } + }, + "components": { + "securitySchemes": { + "apiKeyAuth": { + "type": "apiKey", + "in": "header", + "name": "x-medialit-apikey" + }, + "bearerAuth": { + "type": "http", + "scheme": "bearer", + "bearerFormat": "JWT" + }, + "signatureAuth": { + "type": "apiKey", + "in": "header", + "name": "x-medialit-signature" + } + }, + "schemas": { + "Media": { + "type": "object", + "properties": { + "mediaId": { + "type": "string" + }, + "originalFileName": { + "type": "string" + }, + "mimeType": { + "type": "string" + }, + "size": { + "type": "number", + "format": "float" + }, + "access": { + "type": "string", + "enum": ["public", "private"] + }, + "file": { + "type": "string" + }, + "thumbnail": { + "type": "string" + }, + "caption": { + "type": "string" + }, + "group": { + "type": "string" + } + }, + "required": [ + "mediaId", + "originalFileName", + "mimeType", + "size", + "access", + "file" + ], + "additionalProperties": false + }, + "MediaListItem": { + "type": "object", + "properties": { + "mediaId": { + "type": "string" + }, + "originalFileName": { + "type": "string" + }, + "mimeType": { + "type": "string" + }, + "size": { + "type": "number", + "format": "float" + }, + "access": { + "type": "string", + "enum": ["public", "private"] + }, + "thumbnail": { + "type": "string" + }, + "caption": { + "type": "string" + }, + "group": { + "type": "string" + }, + "createdAt": { + "type": "string", + "format": "date-time" + } + }, + "required": [ + "mediaId", + "originalFileName", + "mimeType", + "size", + "access", + "createdAt" + ], + "additionalProperties": false + }, + "MediaSettings": { + "type": "object", + "properties": { + "useWebP": { + "type": "boolean" + }, + "webpOutputQuality": { + "type": "number", + "format": "float", + "minimum": 0, + "maximum": 100 + }, + "thumbnailWidth": { + "type": "number", + "format": "float", + "minimum": 1 + }, + "thumbnailHeight": { + "type": "number", + "format": "float", + "minimum": 1 + } + }, + "additionalProperties": false + }, + "MediaSettingsPayload": { + "type": "object", + "properties": { + "useWebP": { + "type": "boolean" + }, + "webpOutputQuality": { + "type": "number", + "format": "float", + "minimum": 0, + "maximum": 100 + }, + "thumbnailWidth": { + "type": "number", + "format": "float", + "minimum": 1 + }, + "thumbnailHeight": { + "type": "number", + "format": "float", + "minimum": 1 + } + }, + "additionalProperties": false + }, + "UploadMediaPayload": { + "type": "object", + "properties": { + "caption": { + "type": "string" + }, + "access": { + "type": "string", + "enum": ["public", "private"] + }, + "group": { + "type": "string" + } + }, + "additionalProperties": false + }, + "GetMediaQuery": { + "type": "object", + "properties": { + "page": { + "type": "number", + "format": "float", + "minimum": 1 + }, + "limit": { + "type": "number", + "format": "float", + "minimum": 1 + }, + "access": { + "type": "string", + "enum": ["public", "private"] + }, + "group": { + "type": "string" + } + }, + "additionalProperties": false + }, + "SignatureResponse": { + "type": "object", + "properties": { + "signature": { + "type": "string", + "description": "HMAC signature for secure uploads" + } + }, + "required": ["signature"], + "additionalProperties": false + }, + "MediaCountResponse": { + "type": "object", + "properties": { + "count": { + "type": "number", + "format": "float" + } + }, + "required": ["count"], + "additionalProperties": false + }, + "MediaSizeResponse": { + "type": "object", + "properties": { + "storage": { + "type": "number", + "format": "float" + }, + "maxStorage": { + "type": "number", + "format": "float" + } + }, + "required": ["storage", "maxStorage"], + "additionalProperties": false + }, + "ErrorResponse": { + "type": "object", + "properties": { + "error": { + "type": "string", + "example": "Unauthorized" + } + }, + "required": ["error"], + "additionalProperties": false + } + } + } +} diff --git a/apps/api/src/documentation/openapi/fixtures/sendlit.json b/apps/api/src/documentation/openapi/fixtures/sendlit.json new file mode 100644 index 0000000..2a45333 --- /dev/null +++ b/apps/api/src/documentation/openapi/fixtures/sendlit.json @@ -0,0 +1,13857 @@ +{ + "openapi": "3.0.2", + "paths": { + "/contacts": { + "post": { + "description": "Creates a contact (subscriber). If a contact with the same email already exists for this team, the existing contact is returned.", + "summary": "Create a contact", + "tags": ["Contacts"], + "parameters": [], + "requestBody": { + "description": "Body", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "email": { "type": "string", "format": "email" }, + "name": { "type": "string" }, + "tags": { "type": "array", "items": { "type": "string" } }, + "customFields": { + "default": {}, + "type": "object", + "additionalProperties": { + "oneOf": [ + { + "oneOf": [ + { "type": "string" }, + { "type": "number" }, + { "type": "boolean" } + ] + }, + { + "type": "array", + "items": { + "oneOf": [ + { "type": "string" }, + { "type": "number" }, + { "type": "boolean" } + ] + } + } + ] + } + } + }, + "required": ["email"] + } + } + } + }, + "responses": { + "201": { + "description": "201", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "contactId": { "type": "string" }, + "email": { "type": "string" }, + "name": { "type": "string", "nullable": true }, + "subscribed": { "type": "boolean" }, + "customFields": { + "default": {}, + "type": "object", + "additionalProperties": { + "oneOf": [ + { + "oneOf": [ + { "type": "string" }, + { "type": "number" }, + { "type": "boolean" } + ] + }, + { + "type": "array", + "items": { + "oneOf": [ + { "type": "string" }, + { "type": "number" }, + { "type": "boolean" } + ] + } + } + ] + } + }, + "tags": { "type": "array", "items": { "type": "string" } }, + "unsubscribeToken": { "type": "string" }, + "createdAt": { "type": "string", "nullable": true }, + "updatedAt": { "type": "string", "nullable": true } + }, + "required": [ + "contactId", + "email", + "subscribed", + "tags", + "unsubscribeToken", + "createdAt", + "updatedAt" + ] + } + } + } + }, + "402": { + "description": "402", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + }, + "409": { + "description": "409", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + } + } + }, + "get": { + "description": "Returns a paginated list of contacts. Pass filter as serialized ContactFilterWithAggregator JSON for inline filtering, or segmentId to only return contacts currently matching that saved segment's filter (404 if the segment doesn't exist). SendLit supports fixed generic contact filters over first-class fields, tags, and custom fields; client-specific concepts should be synced into namespaced tags or customFields. q, filter, and segmentId combine with AND. The response's total reflects the combined filters.", + "summary": "List contacts", + "tags": ["Contacts"], + "parameters": [ + { + "name": "q", + "in": "query", + "description": "Search term matched against email or name", + "schema": { "type": "string" } + }, + { + "name": "segmentId", + "in": "query", + "description": "Only return contacts matching this saved segment's filter; combines with q using AND", + "schema": { "type": "string" } + }, + { + "name": "filter", + "in": "query", + "description": "Serialized ContactFilterWithAggregator JSON; combines with q and segmentId using AND", + "schema": { "type": "string" } + }, + { + "name": "offset", + "in": "query", + "schema": { "type": "integer", "minimum": 1 } + }, + { + "name": "rowsPerPage", + "in": "query", + "schema": { "type": "integer", "minimum": 1 } + } + ], + "responses": { + "200": { + "description": "200", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "type": "object", + "properties": { + "contactId": { "type": "string" }, + "email": { "type": "string" }, + "name": { "type": "string", "nullable": true }, + "subscribed": { "type": "boolean" }, + "customFields": { + "default": {}, + "type": "object", + "additionalProperties": { + "oneOf": [ + { + "oneOf": [ + { "type": "string" }, + { "type": "number" }, + { "type": "boolean" } + ] + }, + { + "type": "array", + "items": { + "oneOf": [ + { "type": "string" }, + { "type": "number" }, + { "type": "boolean" } + ] + } + } + ] + } + }, + "tags": { "type": "array", "items": { "type": "string" } }, + "unsubscribeToken": { "type": "string" }, + "createdAt": { "type": "string", "nullable": true }, + "updatedAt": { "type": "string", "nullable": true } + }, + "required": [ + "contactId", + "email", + "subscribed", + "tags", + "unsubscribeToken", + "createdAt", + "updatedAt" + ] + } + }, + "total": { "type": "number" } + }, + "required": ["items", "total"] + } + } + } + }, + "400": { + "description": "400", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + }, + "404": { + "description": "404", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + } + } + } + }, + "/contacts/{contactId}": { + "get": { + "summary": "Get a contact", + "tags": ["Contacts"], + "parameters": [ + { + "name": "contactId", + "in": "path", + "required": true, + "schema": { "type": "string" } + } + ], + "responses": { + "200": { + "description": "200", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "contactId": { "type": "string" }, + "email": { "type": "string" }, + "name": { "type": "string", "nullable": true }, + "subscribed": { "type": "boolean" }, + "customFields": { + "default": {}, + "type": "object", + "additionalProperties": { + "oneOf": [ + { + "oneOf": [ + { "type": "string" }, + { "type": "number" }, + { "type": "boolean" } + ] + }, + { + "type": "array", + "items": { + "oneOf": [ + { "type": "string" }, + { "type": "number" }, + { "type": "boolean" } + ] + } + } + ] + } + }, + "tags": { "type": "array", "items": { "type": "string" } }, + "unsubscribeToken": { "type": "string" }, + "createdAt": { "type": "string", "nullable": true }, + "updatedAt": { "type": "string", "nullable": true } + }, + "required": [ + "contactId", + "email", + "subscribed", + "tags", + "unsubscribeToken", + "createdAt", + "updatedAt" + ] + } + } + } + }, + "404": { + "description": "404", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + } + } + }, + "patch": { + "summary": "Update a contact", + "tags": ["Contacts"], + "parameters": [ + { + "name": "contactId", + "in": "path", + "required": true, + "schema": { "type": "string" } + } + ], + "requestBody": { + "description": "Body", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "name": { "type": "string" }, + "subscribed": { "type": "boolean" }, + "tags": { "type": "array", "items": { "type": "string" } }, + "customFields": { + "default": {}, + "type": "object", + "additionalProperties": { + "oneOf": [ + { + "oneOf": [ + { "type": "string" }, + { "type": "number" }, + { "type": "boolean" } + ] + }, + { + "type": "array", + "items": { + "oneOf": [ + { "type": "string" }, + { "type": "number" }, + { "type": "boolean" } + ] + } + } + ] + } + } + } + } + } + } + }, + "responses": { + "200": { + "description": "200", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "contactId": { "type": "string" }, + "email": { "type": "string" }, + "name": { "type": "string", "nullable": true }, + "subscribed": { "type": "boolean" }, + "customFields": { + "default": {}, + "type": "object", + "additionalProperties": { + "oneOf": [ + { + "oneOf": [ + { "type": "string" }, + { "type": "number" }, + { "type": "boolean" } + ] + }, + { + "type": "array", + "items": { + "oneOf": [ + { "type": "string" }, + { "type": "number" }, + { "type": "boolean" } + ] + } + } + ] + } + }, + "tags": { "type": "array", "items": { "type": "string" } }, + "unsubscribeToken": { "type": "string" }, + "createdAt": { "type": "string", "nullable": true }, + "updatedAt": { "type": "string", "nullable": true } + }, + "required": [ + "contactId", + "email", + "subscribed", + "tags", + "unsubscribeToken", + "createdAt", + "updatedAt" + ] + } + } + } + }, + "402": { + "description": "402", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + }, + "404": { + "description": "404", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + }, + "409": { + "description": "409", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + } + } + }, + "delete": { + "summary": "Delete a contact", + "tags": ["Contacts"], + "parameters": [ + { + "name": "contactId", + "in": "path", + "required": true, + "schema": { "type": "string" } + } + ], + "responses": { "204": { "description": "204" } } + } + }, + "/contacts/{contactId}/tags/{tag}": { + "post": { + "summary": "Add a tag to a contact", + "tags": ["Contacts"], + "parameters": [ + { + "name": "contactId", + "in": "path", + "required": true, + "schema": { "type": "string" } + }, + { + "name": "tag", + "in": "path", + "required": true, + "schema": { "type": "string" } + } + ], + "responses": { + "200": { + "description": "200", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "contactId": { "type": "string" }, + "email": { "type": "string" }, + "name": { "type": "string", "nullable": true }, + "subscribed": { "type": "boolean" }, + "customFields": { + "default": {}, + "type": "object", + "additionalProperties": { + "oneOf": [ + { + "oneOf": [ + { "type": "string" }, + { "type": "number" }, + { "type": "boolean" } + ] + }, + { + "type": "array", + "items": { + "oneOf": [ + { "type": "string" }, + { "type": "number" }, + { "type": "boolean" } + ] + } + } + ] + } + }, + "tags": { "type": "array", "items": { "type": "string" } }, + "unsubscribeToken": { "type": "string" }, + "createdAt": { "type": "string", "nullable": true }, + "updatedAt": { "type": "string", "nullable": true } + }, + "required": [ + "contactId", + "email", + "subscribed", + "tags", + "unsubscribeToken", + "createdAt", + "updatedAt" + ] + } + } + } + }, + "404": { + "description": "404", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + } + } + }, + "delete": { + "summary": "Remove a tag from a contact", + "tags": ["Contacts"], + "parameters": [ + { + "name": "contactId", + "in": "path", + "required": true, + "schema": { "type": "string" } + }, + { + "name": "tag", + "in": "path", + "required": true, + "schema": { "type": "string" } + } + ], + "responses": { + "200": { + "description": "200", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "contactId": { "type": "string" }, + "email": { "type": "string" }, + "name": { "type": "string", "nullable": true }, + "subscribed": { "type": "boolean" }, + "customFields": { + "default": {}, + "type": "object", + "additionalProperties": { + "oneOf": [ + { + "oneOf": [ + { "type": "string" }, + { "type": "number" }, + { "type": "boolean" } + ] + }, + { + "type": "array", + "items": { + "oneOf": [ + { "type": "string" }, + { "type": "number" }, + { "type": "boolean" } + ] + } + } + ] + } + }, + "tags": { "type": "array", "items": { "type": "string" } }, + "unsubscribeToken": { "type": "string" }, + "createdAt": { "type": "string", "nullable": true }, + "updatedAt": { "type": "string", "nullable": true } + }, + "required": [ + "contactId", + "email", + "subscribed", + "tags", + "unsubscribeToken", + "createdAt", + "updatedAt" + ] + } + } + } + }, + "404": { + "description": "404", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + } + } + } + }, + "/contacts/{contactId}/deliveries": { + "get": { + "summary": "List broadcasts/sequence emails delivered to a contact", + "tags": ["Contacts"], + "parameters": [ + { + "name": "contactId", + "in": "path", + "required": true, + "schema": { "type": "string" } + } + ], + "responses": { + "200": { "description": "200" }, + "404": { + "description": "404", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + } + } + } + }, + "/segments": { + "post": { + "summary": "Create a saved segment", + "tags": ["Segments"], + "parameters": [], + "requestBody": { + "description": "Body", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "name": { "type": "string", "minLength": 1 }, + "filter": { + "type": "object", + "properties": { + "aggregator": { "type": "string", "enum": ["and", "or"] }, + "filters": { + "type": "array", + "items": { + "oneOf": [ + { + "type": "object", + "properties": { + "name": { "type": "string", "enum": ["email"] }, + "condition": { + "type": "string", + "enum": ["is", "contains", "not_contains"] + }, + "value": { "type": "string" }, + "valueLabel": { "type": "string" } + }, + "required": ["name", "condition", "value"] + }, + { + "type": "object", + "properties": { + "name": { "type": "string", "enum": ["tag"] }, + "condition": { + "type": "string", + "enum": ["is", "is_not"] + }, + "value": { "type": "string" }, + "valueLabel": { "type": "string" } + }, + "required": ["name", "condition", "value"] + }, + { + "type": "object", + "properties": { + "name": { "type": "string", "enum": ["subscription"] }, + "condition": { "type": "string", "enum": ["is"] }, + "value": { + "type": "string", + "enum": ["subscribed", "unsubscribed"] + }, + "valueLabel": { "type": "string" } + }, + "required": ["name", "condition", "value"] + }, + { + "type": "object", + "properties": { + "name": { "type": "string", "enum": ["signedUp"] }, + "condition": { + "type": "string", + "enum": ["before", "after", "on"] + }, + "value": { "type": "string" }, + "valueLabel": { "type": "string" } + }, + "required": ["name", "condition", "value"] + }, + { + "type": "object", + "properties": { + "name": { "type": "string", "enum": ["customField"] }, + "key": { "type": "string", "minLength": 1 }, + "condition": { + "type": "string", + "enum": [ + "is", + "is_not", + "contains", + "not_contains", + "has", + "not_has", + "before", + "after", + "on", + "exists", + "not_exists" + ] + }, + "value": { "type": "string" }, + "valueLabel": { "type": "string" } + }, + "required": ["name", "key", "condition"] + } + ] + } + } + }, + "required": ["aggregator", "filters"] + } + }, + "required": ["name", "filter"] + } + } + } + }, + "responses": { + "201": { + "description": "201", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "segmentId": { "type": "string" }, + "name": { "type": "string" }, + "filter": { + "type": "object", + "properties": { + "aggregator": { "type": "string", "enum": ["and", "or"] }, + "filters": { + "type": "array", + "items": { + "oneOf": [ + { + "type": "object", + "properties": { + "name": { "type": "string", "enum": ["email"] }, + "condition": { + "type": "string", + "enum": ["is", "contains", "not_contains"] + }, + "value": { "type": "string" }, + "valueLabel": { "type": "string" } + }, + "required": ["name", "condition", "value"] + }, + { + "type": "object", + "properties": { + "name": { "type": "string", "enum": ["tag"] }, + "condition": { + "type": "string", + "enum": ["is", "is_not"] + }, + "value": { "type": "string" }, + "valueLabel": { "type": "string" } + }, + "required": ["name", "condition", "value"] + }, + { + "type": "object", + "properties": { + "name": { + "type": "string", + "enum": ["subscription"] + }, + "condition": { "type": "string", "enum": ["is"] }, + "value": { + "type": "string", + "enum": ["subscribed", "unsubscribed"] + }, + "valueLabel": { "type": "string" } + }, + "required": ["name", "condition", "value"] + }, + { + "type": "object", + "properties": { + "name": { "type": "string", "enum": ["signedUp"] }, + "condition": { + "type": "string", + "enum": ["before", "after", "on"] + }, + "value": { "type": "string" }, + "valueLabel": { "type": "string" } + }, + "required": ["name", "condition", "value"] + }, + { + "type": "object", + "properties": { + "name": { "type": "string", "enum": ["customField"] }, + "key": { "type": "string", "minLength": 1 }, + "condition": { + "type": "string", + "enum": [ + "is", + "is_not", + "contains", + "not_contains", + "has", + "not_has", + "before", + "after", + "on", + "exists", + "not_exists" + ] + }, + "value": { "type": "string" }, + "valueLabel": { "type": "string" } + }, + "required": ["name", "key", "condition"] + } + ] + } + } + }, + "required": ["aggregator", "filters"] + }, + "createdAt": { "type": "string", "nullable": true }, + "updatedAt": { "type": "string", "nullable": true } + }, + "required": ["segmentId", "name", "filter", "createdAt", "updatedAt"] + } + } + } + }, + "409": { + "description": "409", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + } + } + }, + "get": { + "summary": "List saved segments", + "tags": ["Segments"], + "parameters": [], + "responses": { "200": { "description": "200" } } + } + }, + "/segments/{segmentId}": { + "get": { + "summary": "Get a saved segment", + "tags": ["Segments"], + "parameters": [ + { + "name": "segmentId", + "in": "path", + "required": true, + "schema": { "type": "string" } + } + ], + "responses": { + "200": { + "description": "200", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "segmentId": { "type": "string" }, + "name": { "type": "string" }, + "filter": { + "type": "object", + "properties": { + "aggregator": { "type": "string", "enum": ["and", "or"] }, + "filters": { + "type": "array", + "items": { + "oneOf": [ + { + "type": "object", + "properties": { + "name": { "type": "string", "enum": ["email"] }, + "condition": { + "type": "string", + "enum": ["is", "contains", "not_contains"] + }, + "value": { "type": "string" }, + "valueLabel": { "type": "string" } + }, + "required": ["name", "condition", "value"] + }, + { + "type": "object", + "properties": { + "name": { "type": "string", "enum": ["tag"] }, + "condition": { + "type": "string", + "enum": ["is", "is_not"] + }, + "value": { "type": "string" }, + "valueLabel": { "type": "string" } + }, + "required": ["name", "condition", "value"] + }, + { + "type": "object", + "properties": { + "name": { + "type": "string", + "enum": ["subscription"] + }, + "condition": { "type": "string", "enum": ["is"] }, + "value": { + "type": "string", + "enum": ["subscribed", "unsubscribed"] + }, + "valueLabel": { "type": "string" } + }, + "required": ["name", "condition", "value"] + }, + { + "type": "object", + "properties": { + "name": { "type": "string", "enum": ["signedUp"] }, + "condition": { + "type": "string", + "enum": ["before", "after", "on"] + }, + "value": { "type": "string" }, + "valueLabel": { "type": "string" } + }, + "required": ["name", "condition", "value"] + }, + { + "type": "object", + "properties": { + "name": { "type": "string", "enum": ["customField"] }, + "key": { "type": "string", "minLength": 1 }, + "condition": { + "type": "string", + "enum": [ + "is", + "is_not", + "contains", + "not_contains", + "has", + "not_has", + "before", + "after", + "on", + "exists", + "not_exists" + ] + }, + "value": { "type": "string" }, + "valueLabel": { "type": "string" } + }, + "required": ["name", "key", "condition"] + } + ] + } + } + }, + "required": ["aggregator", "filters"] + }, + "createdAt": { "type": "string", "nullable": true }, + "updatedAt": { "type": "string", "nullable": true } + }, + "required": ["segmentId", "name", "filter", "createdAt", "updatedAt"] + } + } + } + }, + "404": { + "description": "404", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + } + } + }, + "patch": { + "summary": "Update a saved segment", + "tags": ["Segments"], + "parameters": [ + { + "name": "segmentId", + "in": "path", + "required": true, + "schema": { "type": "string" } + } + ], + "requestBody": { + "description": "Body", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "name": { "type": "string", "minLength": 1 }, + "filter": { + "type": "object", + "properties": { + "aggregator": { "type": "string", "enum": ["and", "or"] }, + "filters": { + "type": "array", + "items": { + "oneOf": [ + { + "type": "object", + "properties": { + "name": { "type": "string", "enum": ["email"] }, + "condition": { + "type": "string", + "enum": ["is", "contains", "not_contains"] + }, + "value": { "type": "string" }, + "valueLabel": { "type": "string" } + }, + "required": ["name", "condition", "value"] + }, + { + "type": "object", + "properties": { + "name": { "type": "string", "enum": ["tag"] }, + "condition": { + "type": "string", + "enum": ["is", "is_not"] + }, + "value": { "type": "string" }, + "valueLabel": { "type": "string" } + }, + "required": ["name", "condition", "value"] + }, + { + "type": "object", + "properties": { + "name": { "type": "string", "enum": ["subscription"] }, + "condition": { "type": "string", "enum": ["is"] }, + "value": { + "type": "string", + "enum": ["subscribed", "unsubscribed"] + }, + "valueLabel": { "type": "string" } + }, + "required": ["name", "condition", "value"] + }, + { + "type": "object", + "properties": { + "name": { "type": "string", "enum": ["signedUp"] }, + "condition": { + "type": "string", + "enum": ["before", "after", "on"] + }, + "value": { "type": "string" }, + "valueLabel": { "type": "string" } + }, + "required": ["name", "condition", "value"] + }, + { + "type": "object", + "properties": { + "name": { "type": "string", "enum": ["customField"] }, + "key": { "type": "string", "minLength": 1 }, + "condition": { + "type": "string", + "enum": [ + "is", + "is_not", + "contains", + "not_contains", + "has", + "not_has", + "before", + "after", + "on", + "exists", + "not_exists" + ] + }, + "value": { "type": "string" }, + "valueLabel": { "type": "string" } + }, + "required": ["name", "key", "condition"] + } + ] + } + } + }, + "required": ["aggregator", "filters"] + } + } + } + } + } + }, + "responses": { + "200": { + "description": "200", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "segmentId": { "type": "string" }, + "name": { "type": "string" }, + "filter": { + "type": "object", + "properties": { + "aggregator": { "type": "string", "enum": ["and", "or"] }, + "filters": { + "type": "array", + "items": { + "oneOf": [ + { + "type": "object", + "properties": { + "name": { "type": "string", "enum": ["email"] }, + "condition": { + "type": "string", + "enum": ["is", "contains", "not_contains"] + }, + "value": { "type": "string" }, + "valueLabel": { "type": "string" } + }, + "required": ["name", "condition", "value"] + }, + { + "type": "object", + "properties": { + "name": { "type": "string", "enum": ["tag"] }, + "condition": { + "type": "string", + "enum": ["is", "is_not"] + }, + "value": { "type": "string" }, + "valueLabel": { "type": "string" } + }, + "required": ["name", "condition", "value"] + }, + { + "type": "object", + "properties": { + "name": { + "type": "string", + "enum": ["subscription"] + }, + "condition": { "type": "string", "enum": ["is"] }, + "value": { + "type": "string", + "enum": ["subscribed", "unsubscribed"] + }, + "valueLabel": { "type": "string" } + }, + "required": ["name", "condition", "value"] + }, + { + "type": "object", + "properties": { + "name": { "type": "string", "enum": ["signedUp"] }, + "condition": { + "type": "string", + "enum": ["before", "after", "on"] + }, + "value": { "type": "string" }, + "valueLabel": { "type": "string" } + }, + "required": ["name", "condition", "value"] + }, + { + "type": "object", + "properties": { + "name": { "type": "string", "enum": ["customField"] }, + "key": { "type": "string", "minLength": 1 }, + "condition": { + "type": "string", + "enum": [ + "is", + "is_not", + "contains", + "not_contains", + "has", + "not_has", + "before", + "after", + "on", + "exists", + "not_exists" + ] + }, + "value": { "type": "string" }, + "valueLabel": { "type": "string" } + }, + "required": ["name", "key", "condition"] + } + ] + } + } + }, + "required": ["aggregator", "filters"] + }, + "createdAt": { "type": "string", "nullable": true }, + "updatedAt": { "type": "string", "nullable": true } + }, + "required": ["segmentId", "name", "filter", "createdAt", "updatedAt"] + } + } + } + }, + "404": { + "description": "404", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + }, + "409": { + "description": "409", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + } + } + }, + "delete": { + "summary": "Delete a saved segment", + "tags": ["Segments"], + "parameters": [ + { + "name": "segmentId", + "in": "path", + "required": true, + "schema": { "type": "string" } + } + ], + "responses": { "204": { "description": "204" } } + } + }, + "/media": { + "get": { + "description": "Returns the team's uploaded MediaLit-backed media. Unsplash and external URL images are not stored here.", + "summary": "List uploaded media", + "tags": ["Media"], + "parameters": [ + { + "name": "page", + "in": "query", + "schema": { "type": "integer", "minimum": 1 } + }, + { + "name": "pageSize", + "in": "query", + "schema": { "type": "integer", "minimum": 1, "maximum": 100 } + }, + { "name": "query", "in": "query", "schema": { "type": "string" } } + ], + "responses": { + "200": { + "description": "200", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "type": "object", + "properties": { + "mediaId": { "type": "string" }, + "url": { "type": "string" }, + "thumbnailUrl": { "type": "string", "nullable": true }, + "mediaLitId": { "type": "string" }, + "fileName": { "type": "string", "nullable": true }, + "mimeType": { "type": "string", "nullable": true }, + "size": { "type": "number", "nullable": true }, + "width": { "type": "number", "nullable": true }, + "height": { "type": "number", "nullable": true }, + "alt": { "type": "string", "nullable": true }, + "caption": { "type": "string", "nullable": true }, + "createdAt": { + "oneOf": [ + { "type": "string" }, + { "type": "string", "format": "date-time" } + ], + "nullable": true + }, + "updatedAt": { + "oneOf": [ + { "type": "string" }, + { "type": "string", "format": "date-time" } + ], + "nullable": true + } + }, + "required": ["mediaId", "url", "mediaLitId"] + } + }, + "total": { "type": "number" } + }, + "required": ["items", "total"] + } + } + } + } + } + } + }, + "/media/presigned": { + "post": { + "description": "Returns a short-lived MediaLit signature and endpoint. Upload image files directly to `${endpoint}/media/create/resumable` with the signature in the `x-medialit-signature` header.", + "summary": "Generate a MediaLit upload signature", + "tags": ["Media"], + "parameters": [], + "responses": { + "200": { + "description": "200", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "signature": { "type": "string" }, + "endpoint": { "type": "string" } + }, + "required": ["signature", "endpoint"] + } + } + } + }, + "500": { + "description": "500", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + } + } + } + }, + "/media/{mediaId}": { + "get": { + "summary": "Get uploaded media", + "tags": ["Media"], + "parameters": [ + { + "name": "mediaId", + "in": "path", + "required": true, + "schema": { "type": "string" } + } + ], + "responses": { + "200": { + "description": "200", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "mediaId": { "type": "string" }, + "url": { "type": "string" }, + "thumbnailUrl": { "type": "string", "nullable": true }, + "mediaLitId": { "type": "string" }, + "fileName": { "type": "string", "nullable": true }, + "mimeType": { "type": "string", "nullable": true }, + "size": { "type": "number", "nullable": true }, + "width": { "type": "number", "nullable": true }, + "height": { "type": "number", "nullable": true }, + "alt": { "type": "string", "nullable": true }, + "caption": { "type": "string", "nullable": true }, + "createdAt": { + "oneOf": [ + { "type": "string" }, + { "type": "string", "format": "date-time" } + ], + "nullable": true + }, + "updatedAt": { + "oneOf": [ + { "type": "string" }, + { "type": "string", "format": "date-time" } + ], + "nullable": true + } + }, + "required": ["mediaId", "url", "mediaLitId"] + } + } + } + }, + "404": { + "description": "404", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + } + } + }, + "patch": { + "summary": "Update uploaded media metadata", + "tags": ["Media"], + "parameters": [ + { + "name": "mediaId", + "in": "path", + "required": true, + "schema": { "type": "string" } + } + ], + "requestBody": { + "description": "Body", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "alt": { "type": "string", "nullable": true }, + "caption": { "type": "string", "nullable": true } + } + } + } + } + }, + "responses": { + "200": { + "description": "200", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "mediaId": { "type": "string" }, + "url": { "type": "string" }, + "thumbnailUrl": { "type": "string", "nullable": true }, + "mediaLitId": { "type": "string" }, + "fileName": { "type": "string", "nullable": true }, + "mimeType": { "type": "string", "nullable": true }, + "size": { "type": "number", "nullable": true }, + "width": { "type": "number", "nullable": true }, + "height": { "type": "number", "nullable": true }, + "alt": { "type": "string", "nullable": true }, + "caption": { "type": "string", "nullable": true }, + "createdAt": { + "oneOf": [ + { "type": "string" }, + { "type": "string", "format": "date-time" } + ], + "nullable": true + }, + "updatedAt": { + "oneOf": [ + { "type": "string" }, + { "type": "string", "format": "date-time" } + ], + "nullable": true + } + }, + "required": ["mediaId", "url", "mediaLitId"] + } + } + } + }, + "404": { + "description": "404", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + } + } + }, + "delete": { + "description": "Deletes the MediaLit file and SendLit media row only when no saved email content references it.", + "summary": "Delete unused uploaded media", + "tags": ["Media"], + "parameters": [ + { + "name": "mediaId", + "in": "path", + "required": true, + "schema": { "type": "string" } + } + ], + "responses": { + "204": { "description": "204" }, + "404": { + "description": "404", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + }, + "409": { + "description": "409", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + } + } + } + }, + "/media/{mediaId}/references": { + "get": { + "summary": "List uploaded media references", + "tags": ["Media"], + "parameters": [ + { + "name": "mediaId", + "in": "path", + "required": true, + "schema": { "type": "string" } + } + ], + "responses": { + "200": { + "description": "200", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "type": "object", + "properties": { + "resourceType": { + "type": "string", + "enum": ["TEMPLATE", "SEQUENCE_EMAIL"] + }, + "resourcePublicId": { "type": "string" }, + "parentResourcePublicId": { + "type": "string", + "nullable": true + }, + "createdAt": { + "oneOf": [ + { "type": "string" }, + { "type": "string", "format": "date-time" } + ], + "nullable": true + }, + "updatedAt": { + "oneOf": [ + { "type": "string" }, + { "type": "string", "format": "date-time" } + ], + "nullable": true + } + }, + "required": ["resourceType", "resourcePublicId"] + } + } + }, + "required": ["items"] + } + } + } + }, + "404": { + "description": "404", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + } + } + } + }, + "/system-templates": { + "get": { + "description": "Not team-scoped—the same for every team. Filter by purpose. System templates are starters and must be duplicated into a team-owned template before transactional sending.", + "summary": "List built-in starting templates", + "tags": ["Templates"], + "parameters": [ + { + "name": "purpose", + "in": "query", + "description": "Immutable sending purpose. Marketing templates require a final managed footer; transactional templates are footer-free and are valid for POST /emails.", + "schema": { "type": "string", "enum": ["marketing", "transactional"] } + } + ], + "responses": { + "200": { + "description": "200", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "type": "object", + "properties": { + "templateId": { "type": "string" }, + "title": { "type": "string" }, + "description": { "type": "string" }, + "purpose": { + "type": "string", + "enum": ["marketing", "transactional"], + "description": "Immutable sending purpose. Marketing templates require a final managed footer; transactional templates are footer-free and are valid for POST /emails." + }, + "content": { + "type": "object", + "properties": { + "style": { + "type": "object", + "additionalProperties": { "nullable": true } + }, + "meta": { + "type": "object", + "additionalProperties": { "nullable": true } + }, + "content": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": { "nullable": true } + } + } + }, + "required": ["style", "meta", "content"] + }, + "requiredVariables": { + "type": "array", + "items": { "type": "string" }, + "description": "Sorted, server-discovered Liquid paths that must be supplied." + }, + "variableDefinitions": { + "type": "array", + "items": { + "type": "object", + "properties": { + "path": { + "type": "string", + "minLength": 1, + "description": "Dot-separated Liquid variable path." + }, + "description": { + "type": "string", + "description": "Human-readable value description." + }, + "example": { + "description": "Example value used in generated requests.", + "nullable": true + } + }, + "required": ["path", "description"] + }, + "description": "Documentation metadata for built-in starter variables; runtime discovery remains authoritative." + } + }, + "required": [ + "templateId", + "title", + "description", + "purpose", + "content", + "requiredVariables" + ] + } + } + }, + "required": ["items"] + } + } + } + } + } + } + }, + "/templates": { + "post": { + "description": "Purpose is immutable. Marketing content must contain exactly one final managed footer; transactional content must not contain a footer or marketing-only variables.", + "summary": "Create an email template", + "tags": ["Templates"], + "parameters": [], + "requestBody": { + "description": "Body", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "title": { "type": "string", "minLength": 1 }, + "purpose": { + "default": "marketing", + "type": "string", + "enum": ["marketing", "transactional"], + "description": "Immutable sending purpose. Marketing templates require a final managed footer; transactional templates are footer-free and are valid for POST /emails." + }, + "content": { + "type": "object", + "properties": { + "style": { + "type": "object", + "properties": { + "colors": { + "type": "object", + "properties": { + "background": { + "type": "string", + "pattern": "^#(?:[\\da-f]{3,4}|[\\da-f]{6}|[\\da-f]{8})$" + }, + "foreground": { + "type": "string", + "pattern": "^#(?:[\\da-f]{3,4}|[\\da-f]{6}|[\\da-f]{8})$" + }, + "border": { + "type": "string", + "pattern": "^#(?:[\\da-f]{3,4}|[\\da-f]{6}|[\\da-f]{8})$" + }, + "accent": { + "type": "string", + "pattern": "^#(?:[\\da-f]{3,4}|[\\da-f]{6}|[\\da-f]{8})$" + }, + "accentForeground": { + "type": "string", + "pattern": "^#(?:[\\da-f]{3,4}|[\\da-f]{6}|[\\da-f]{8})$" + } + }, + "required": [ + "background", + "foreground", + "border", + "accent", + "accentForeground" + ], + "additionalProperties": false + }, + "typography": { + "type": "object", + "properties": { + "header": { + "type": "object", + "properties": {}, + "additionalProperties": true + }, + "text": { + "type": "object", + "properties": {}, + "additionalProperties": true + }, + "link": { + "type": "object", + "properties": {}, + "additionalProperties": true + } + }, + "required": ["header", "text", "link"], + "additionalProperties": false + }, + "interactives": { + "type": "object", + "properties": { + "button": { + "type": "object", + "properties": { + "padding": { + "type": "object", + "properties": { + "x": { + "type": "string", + "pattern": "^\\d+(?:\\.\\d+)?px$" + }, + "y": { + "type": "string", + "pattern": "^\\d+(?:\\.\\d+)?px$" + } + } + }, + "border": { + "type": "object", + "properties": { + "width": { + "type": "string", + "pattern": "^\\d+(?:\\.\\d+)?px$" + }, + "radius": { + "type": "string", + "pattern": "^\\d+(?:\\.\\d+)?px$" + }, + "style": { "type": "string", "minLength": 1 } + } + } + }, + "additionalProperties": true + }, + "link": { + "type": "object", + "properties": { + "padding": { + "type": "object", + "properties": { + "x": { + "type": "string", + "pattern": "^\\d+(?:\\.\\d+)?px$" + }, + "y": { + "type": "string", + "pattern": "^\\d+(?:\\.\\d+)?px$" + } + } + } + }, + "additionalProperties": true + } + }, + "required": ["button", "link"], + "additionalProperties": false + }, + "structure": { + "type": "object", + "properties": { + "page": { + "type": "object", + "properties": { + "background": { + "type": "string", + "pattern": "^#(?:[\\da-f]{3,4}|[\\da-f]{6}|[\\da-f]{8})$" + }, + "foreground": { + "type": "string", + "pattern": "^#(?:[\\da-f]{3,4}|[\\da-f]{6}|[\\da-f]{8})$" + }, + "width": { + "type": "string", + "pattern": "^\\d+(?:\\.\\d+)?px$" + }, + "marginY": { + "type": "string", + "pattern": "^\\d+(?:\\.\\d+)?px$" + }, + "borderWidth": { + "type": "string", + "pattern": "^\\d+(?:\\.\\d+)?px$" + }, + "borderStyle": { "type": "string", "minLength": 1 }, + "borderRadius": { + "type": "string", + "pattern": "^\\d+(?:\\.\\d+)?px$" + } + }, + "additionalProperties": false + }, + "section": { + "type": "object", + "properties": { + "padding": { + "type": "object", + "properties": { + "x": { + "type": "string", + "pattern": "^\\d+(?:\\.\\d+)?px$" + }, + "y": { + "type": "string", + "pattern": "^\\d+(?:\\.\\d+)?px$" + } + } + } + }, + "additionalProperties": false + } + }, + "required": ["page", "section"], + "additionalProperties": false + } + }, + "required": [ + "colors", + "typography", + "interactives", + "structure" + ], + "additionalProperties": false + }, + "meta": { + "type": "object", + "properties": { + "previewText": { "type": "string" }, + "utm": { + "type": "object", + "properties": { + "source": { "type": "string" }, + "medium": { "type": "string" }, + "campaign": { "type": "string" } + }, + "required": ["source", "medium", "campaign"], + "additionalProperties": false + } + }, + "additionalProperties": false + }, + "content": { + "type": "array", + "items": { + "discriminator": { "propertyName": "blockType" }, + "oneOf": [ + { + "type": "object", + "properties": { + "id": { "type": "string" }, + "settings": { + "type": "object", + "properties": { "content": { "type": "string" } }, + "required": ["content"], + "additionalProperties": true + }, + "blockType": { "type": "string", "enum": ["text"] } + }, + "required": ["settings", "blockType"] + }, + { + "type": "object", + "properties": { + "id": { "type": "string" }, + "settings": { + "type": "object", + "properties": { + "text": { "type": "string" }, + "url": { "type": "string", "minLength": 1 } + }, + "required": ["text", "url"], + "additionalProperties": true + }, + "blockType": { "type": "string", "enum": ["link"] } + }, + "required": ["settings", "blockType"] + }, + { + "type": "object", + "properties": { + "id": { "type": "string" }, + "settings": { + "type": "object", + "properties": { + "src": { "type": "string", "minLength": 1 } + }, + "required": ["src"], + "additionalProperties": true + }, + "blockType": { "type": "string", "enum": ["image"] } + }, + "required": ["settings", "blockType"] + }, + { + "type": "object", + "properties": { + "id": { "type": "string" }, + "settings": { + "type": "object", + "additionalProperties": { "nullable": true } + }, + "blockType": { "type": "string", "enum": ["separator"] } + }, + "required": ["settings", "blockType"] + }, + { + "type": "object", + "properties": { + "id": { "type": "string" }, + "settings": { + "type": "object", + "additionalProperties": { "nullable": true } + }, + "blockType": { "type": "string", "enum": ["footer"] } + }, + "required": ["settings", "blockType"] + } + ] + } + } + }, + "required": ["style", "meta", "content"] + } + }, + "required": ["title", "content"] + } + } + } + }, + "responses": { + "201": { + "description": "201", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "templateId": { "type": "string" }, + "title": { "type": "string" }, + "purpose": { + "type": "string", + "enum": ["marketing", "transactional"], + "description": "Immutable sending purpose. Marketing templates require a final managed footer; transactional templates are footer-free and are valid for POST /emails." + }, + "content": { + "type": "object", + "properties": { + "style": { + "type": "object", + "additionalProperties": { "nullable": true } + }, + "meta": { + "type": "object", + "additionalProperties": { "nullable": true } + }, + "content": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": { "nullable": true } + } + } + }, + "required": ["style", "meta", "content"] + }, + "requiredVariables": { + "type": "array", + "items": { "type": "string" }, + "description": "Sorted, server-discovered Liquid paths that must be supplied. Defaults and inactive guarded branches are omitted." + }, + "validationError": { + "type": "string", + "description": "Present only for a retired template that predates the managed-footer format. It cannot be edited, duplicated, or sent; recreate it instead." + }, + "createdAt": { "type": "string", "nullable": true }, + "updatedAt": { "type": "string", "nullable": true } + }, + "required": [ + "templateId", + "title", + "purpose", + "content", + "requiredVariables", + "createdAt", + "updatedAt" + ] + } + } + } + }, + "422": { + "description": "422", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { "type": "string" }, + "variables": { "type": "array", "items": { "type": "string" } } + }, + "required": ["error"] + } + } + } + } + } + }, + "get": { + "description": "Returns team-owned templates with their immutable purpose and server-computed requiredVariables. Optionally filter by purpose.", + "summary": "List email templates", + "tags": ["Templates"], + "parameters": [ + { + "name": "purpose", + "in": "query", + "description": "Immutable sending purpose. Marketing templates require a final managed footer; transactional templates are footer-free and are valid for POST /emails.", + "schema": { "type": "string", "enum": ["marketing", "transactional"] } + } + ], + "responses": { "200": { "description": "200" } } + } + }, + "/templates/{templateId}": { + "get": { + "summary": "Get an email template", + "tags": ["Templates"], + "parameters": [ + { + "name": "templateId", + "in": "path", + "required": true, + "schema": { "type": "string" } + } + ], + "responses": { + "200": { + "description": "200", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "templateId": { "type": "string" }, + "title": { "type": "string" }, + "purpose": { + "type": "string", + "enum": ["marketing", "transactional"], + "description": "Immutable sending purpose. Marketing templates require a final managed footer; transactional templates are footer-free and are valid for POST /emails." + }, + "content": { + "type": "object", + "properties": { + "style": { + "type": "object", + "additionalProperties": { "nullable": true } + }, + "meta": { + "type": "object", + "additionalProperties": { "nullable": true } + }, + "content": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": { "nullable": true } + } + } + }, + "required": ["style", "meta", "content"] + }, + "requiredVariables": { + "type": "array", + "items": { "type": "string" }, + "description": "Sorted, server-discovered Liquid paths that must be supplied. Defaults and inactive guarded branches are omitted." + }, + "validationError": { + "type": "string", + "description": "Present only for a retired template that predates the managed-footer format. It cannot be edited, duplicated, or sent; recreate it instead." + }, + "createdAt": { "type": "string", "nullable": true }, + "updatedAt": { "type": "string", "nullable": true } + }, + "required": [ + "templateId", + "title", + "purpose", + "content", + "requiredVariables", + "createdAt", + "updatedAt" + ] + } + } + } + }, + "404": { + "description": "404", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + }, + "422": { + "description": "422", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { "type": "string" }, + "variables": { "type": "array", "items": { "type": "string" } } + }, + "required": ["error"] + } + } + } + } + } + }, + "patch": { + "summary": "Update an email template", + "tags": ["Templates"], + "parameters": [ + { + "name": "templateId", + "in": "path", + "required": true, + "schema": { "type": "string" } + } + ], + "requestBody": { + "description": "Body", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "title": { "type": "string", "minLength": 1 }, + "content": { + "type": "object", + "properties": { + "style": { + "type": "object", + "properties": { + "colors": { + "type": "object", + "properties": { + "background": { + "type": "string", + "pattern": "^#(?:[\\da-f]{3,4}|[\\da-f]{6}|[\\da-f]{8})$" + }, + "foreground": { + "type": "string", + "pattern": "^#(?:[\\da-f]{3,4}|[\\da-f]{6}|[\\da-f]{8})$" + }, + "border": { + "type": "string", + "pattern": "^#(?:[\\da-f]{3,4}|[\\da-f]{6}|[\\da-f]{8})$" + }, + "accent": { + "type": "string", + "pattern": "^#(?:[\\da-f]{3,4}|[\\da-f]{6}|[\\da-f]{8})$" + }, + "accentForeground": { + "type": "string", + "pattern": "^#(?:[\\da-f]{3,4}|[\\da-f]{6}|[\\da-f]{8})$" + } + }, + "required": [ + "background", + "foreground", + "border", + "accent", + "accentForeground" + ], + "additionalProperties": false + }, + "typography": { + "type": "object", + "properties": { + "header": { + "type": "object", + "properties": {}, + "additionalProperties": true + }, + "text": { + "type": "object", + "properties": {}, + "additionalProperties": true + }, + "link": { + "type": "object", + "properties": {}, + "additionalProperties": true + } + }, + "required": ["header", "text", "link"], + "additionalProperties": false + }, + "interactives": { + "type": "object", + "properties": { + "button": { + "type": "object", + "properties": { + "padding": { + "type": "object", + "properties": { + "x": { + "type": "string", + "pattern": "^\\d+(?:\\.\\d+)?px$" + }, + "y": { + "type": "string", + "pattern": "^\\d+(?:\\.\\d+)?px$" + } + } + }, + "border": { + "type": "object", + "properties": { + "width": { + "type": "string", + "pattern": "^\\d+(?:\\.\\d+)?px$" + }, + "radius": { + "type": "string", + "pattern": "^\\d+(?:\\.\\d+)?px$" + }, + "style": { "type": "string", "minLength": 1 } + } + } + }, + "additionalProperties": true + }, + "link": { + "type": "object", + "properties": { + "padding": { + "type": "object", + "properties": { + "x": { + "type": "string", + "pattern": "^\\d+(?:\\.\\d+)?px$" + }, + "y": { + "type": "string", + "pattern": "^\\d+(?:\\.\\d+)?px$" + } + } + } + }, + "additionalProperties": true + } + }, + "required": ["button", "link"], + "additionalProperties": false + }, + "structure": { + "type": "object", + "properties": { + "page": { + "type": "object", + "properties": { + "background": { + "type": "string", + "pattern": "^#(?:[\\da-f]{3,4}|[\\da-f]{6}|[\\da-f]{8})$" + }, + "foreground": { + "type": "string", + "pattern": "^#(?:[\\da-f]{3,4}|[\\da-f]{6}|[\\da-f]{8})$" + }, + "width": { + "type": "string", + "pattern": "^\\d+(?:\\.\\d+)?px$" + }, + "marginY": { + "type": "string", + "pattern": "^\\d+(?:\\.\\d+)?px$" + }, + "borderWidth": { + "type": "string", + "pattern": "^\\d+(?:\\.\\d+)?px$" + }, + "borderStyle": { "type": "string", "minLength": 1 }, + "borderRadius": { + "type": "string", + "pattern": "^\\d+(?:\\.\\d+)?px$" + } + }, + "additionalProperties": false + }, + "section": { + "type": "object", + "properties": { + "padding": { + "type": "object", + "properties": { + "x": { + "type": "string", + "pattern": "^\\d+(?:\\.\\d+)?px$" + }, + "y": { + "type": "string", + "pattern": "^\\d+(?:\\.\\d+)?px$" + } + } + } + }, + "additionalProperties": false + } + }, + "required": ["page", "section"], + "additionalProperties": false + } + }, + "required": [ + "colors", + "typography", + "interactives", + "structure" + ], + "additionalProperties": false + }, + "meta": { + "type": "object", + "properties": { + "previewText": { "type": "string" }, + "utm": { + "type": "object", + "properties": { + "source": { "type": "string" }, + "medium": { "type": "string" }, + "campaign": { "type": "string" } + }, + "required": ["source", "medium", "campaign"], + "additionalProperties": false + } + }, + "additionalProperties": false + }, + "content": { + "type": "array", + "items": { + "discriminator": { "propertyName": "blockType" }, + "oneOf": [ + { + "type": "object", + "properties": { + "id": { "type": "string" }, + "settings": { + "type": "object", + "properties": { "content": { "type": "string" } }, + "required": ["content"], + "additionalProperties": true + }, + "blockType": { "type": "string", "enum": ["text"] } + }, + "required": ["settings", "blockType"] + }, + { + "type": "object", + "properties": { + "id": { "type": "string" }, + "settings": { + "type": "object", + "properties": { + "text": { "type": "string" }, + "url": { "type": "string", "minLength": 1 } + }, + "required": ["text", "url"], + "additionalProperties": true + }, + "blockType": { "type": "string", "enum": ["link"] } + }, + "required": ["settings", "blockType"] + }, + { + "type": "object", + "properties": { + "id": { "type": "string" }, + "settings": { + "type": "object", + "properties": { + "src": { "type": "string", "minLength": 1 } + }, + "required": ["src"], + "additionalProperties": true + }, + "blockType": { "type": "string", "enum": ["image"] } + }, + "required": ["settings", "blockType"] + }, + { + "type": "object", + "properties": { + "id": { "type": "string" }, + "settings": { + "type": "object", + "additionalProperties": { "nullable": true } + }, + "blockType": { "type": "string", "enum": ["separator"] } + }, + "required": ["settings", "blockType"] + }, + { + "type": "object", + "properties": { + "id": { "type": "string" }, + "settings": { + "type": "object", + "additionalProperties": { "nullable": true } + }, + "blockType": { "type": "string", "enum": ["footer"] } + }, + "required": ["settings", "blockType"] + } + ] + } + } + }, + "required": ["style", "meta", "content"] + } + } + } + } + } + }, + "responses": { + "200": { + "description": "200", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "templateId": { "type": "string" }, + "title": { "type": "string" }, + "purpose": { + "type": "string", + "enum": ["marketing", "transactional"], + "description": "Immutable sending purpose. Marketing templates require a final managed footer; transactional templates are footer-free and are valid for POST /emails." + }, + "content": { + "type": "object", + "properties": { + "style": { + "type": "object", + "additionalProperties": { "nullable": true } + }, + "meta": { + "type": "object", + "additionalProperties": { "nullable": true } + }, + "content": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": { "nullable": true } + } + } + }, + "required": ["style", "meta", "content"] + }, + "requiredVariables": { + "type": "array", + "items": { "type": "string" }, + "description": "Sorted, server-discovered Liquid paths that must be supplied. Defaults and inactive guarded branches are omitted." + }, + "validationError": { + "type": "string", + "description": "Present only for a retired template that predates the managed-footer format. It cannot be edited, duplicated, or sent; recreate it instead." + }, + "createdAt": { "type": "string", "nullable": true }, + "updatedAt": { "type": "string", "nullable": true } + }, + "required": [ + "templateId", + "title", + "purpose", + "content", + "requiredVariables", + "createdAt", + "updatedAt" + ] + } + } + } + }, + "404": { + "description": "404", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + }, + "409": { + "description": "409", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + }, + "422": { + "description": "422", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { "type": "string" }, + "variables": { "type": "array", "items": { "type": "string" } } + }, + "required": ["error"] + } + } + } + } + } + }, + "delete": { + "summary": "Delete an email template", + "tags": ["Templates"], + "parameters": [ + { + "name": "templateId", + "in": "path", + "required": true, + "schema": { "type": "string" } + } + ], + "responses": { "204": { "description": "204" } } + } + }, + "/templates/{templateId}/duplicate": { + "post": { + "description": "Creates a new team-owned copy with the same immutable purpose as the source template.", + "summary": "Duplicate an email template", + "tags": ["Templates"], + "parameters": [ + { + "name": "templateId", + "in": "path", + "required": true, + "schema": { "type": "string" } + } + ], + "requestBody": { + "description": "Body", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "title": { "type": "string", "minLength": 1 } } + } + } + } + }, + "responses": { + "201": { + "description": "201", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "templateId": { "type": "string" }, + "title": { "type": "string" }, + "purpose": { + "type": "string", + "enum": ["marketing", "transactional"], + "description": "Immutable sending purpose. Marketing templates require a final managed footer; transactional templates are footer-free and are valid for POST /emails." + }, + "content": { + "type": "object", + "properties": { + "style": { + "type": "object", + "additionalProperties": { "nullable": true } + }, + "meta": { + "type": "object", + "additionalProperties": { "nullable": true } + }, + "content": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": { "nullable": true } + } + } + }, + "required": ["style", "meta", "content"] + }, + "requiredVariables": { + "type": "array", + "items": { "type": "string" }, + "description": "Sorted, server-discovered Liquid paths that must be supplied. Defaults and inactive guarded branches are omitted." + }, + "validationError": { + "type": "string", + "description": "Present only for a retired template that predates the managed-footer format. It cannot be edited, duplicated, or sent; recreate it instead." + }, + "createdAt": { "type": "string", "nullable": true }, + "updatedAt": { "type": "string", "nullable": true } + }, + "required": [ + "templateId", + "title", + "purpose", + "content", + "requiredVariables", + "createdAt", + "updatedAt" + ] + } + } + } + }, + "404": { + "description": "404", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + }, + "422": { + "description": "422", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { "type": "string" }, + "variables": { "type": "array", "items": { "type": "string" } } + }, + "required": ["error"] + } + } + } + } + } + } + }, + "/sequences": { + "post": { + "description": "templateId must identify a marketing team template or marketing system starter. Transactional templates return 422 template_not_marketing.", + "summary": "Create a broadcast or a sequence", + "tags": ["Sequences"], + "parameters": [], + "requestBody": { + "description": "Body", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "type": { "type": "string", "enum": ["broadcast", "sequence"] }, + "templateId": { "type": "string", "minLength": 1 }, + "deliverySource": { + "discriminator": { "propertyName": "type" }, + "oneOf": [ + { + "type": "object", + "properties": { + "type": { "type": "string", "enum": ["organization"] } + }, + "required": ["type"] + }, + { + "type": "object", + "properties": { + "type": { "type": "string", "enum": ["team"] }, + "espId": { "type": "string", "minLength": 1 } + }, + "required": ["type"] + } + ] + } + }, + "required": ["type", "templateId"] + } + } + } + }, + "responses": { + "201": { + "description": "201", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "sequenceId": { "type": "string" }, + "type": { "type": "string" }, + "title": { "type": "string" }, + "status": { "type": "string" }, + "deliverySource": { + "discriminator": { "propertyName": "type" }, + "oneOf": [ + { + "type": "object", + "properties": { + "type": { "type": "string", "enum": ["organization"] } + }, + "required": ["type"] + }, + { + "type": "object", + "properties": { + "type": { "type": "string", "enum": ["team"] }, + "espId": { "type": "string", "minLength": 1 } + }, + "required": ["type"] + } + ], + "nullable": true + }, + "triggerType": { "type": "string", "nullable": true }, + "triggerData": { "type": "string", "nullable": true }, + "filter": { "nullable": true }, + "excludeFilter": { "nullable": true }, + "emailsOrder": { "type": "array", "items": { "type": "string" } }, + "entrants": { "type": "array", "items": { "type": "string" } }, + "report": { "nullable": true }, + "createdAt": { "type": "string", "nullable": true }, + "updatedAt": { "type": "string", "nullable": true }, + "emails": { + "type": "array", + "items": { + "type": "object", + "properties": { + "emailId": { "type": "string" }, + "subject": { "type": "string" }, + "content": { + "type": "object", + "properties": { + "style": { + "type": "object", + "additionalProperties": { "nullable": true } + }, + "meta": { + "type": "object", + "additionalProperties": { "nullable": true } + }, + "content": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": { "nullable": true } + } + } + }, + "required": ["style", "meta", "content"] + }, + "delayInMillis": { "type": "number" }, + "published": { "type": "boolean" }, + "templateId": { "type": "string", "nullable": true }, + "actionType": { "type": "string", "nullable": true }, + "actionData": { + "type": "object", + "additionalProperties": { "nullable": true }, + "nullable": true + }, + "createdAt": { "type": "string", "nullable": true }, + "updatedAt": { "type": "string", "nullable": true } + }, + "required": [ + "emailId", + "subject", + "content", + "delayInMillis", + "published", + "createdAt", + "updatedAt" + ] + } + } + }, + "required": [ + "sequenceId", + "type", + "title", + "status", + "deliverySource", + "emailsOrder", + "entrants", + "createdAt", + "updatedAt", + "emails" + ] + } + } + } + }, + "400": { + "description": "400", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + }, + "422": { + "description": "422", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { "type": "string", "enum": ["template_not_marketing"] } + }, + "required": ["error"] + } + } + } + } + } + }, + "get": { + "summary": "List broadcasts or sequences", + "tags": ["Sequences"], + "parameters": [ + { + "name": "type", + "in": "query", + "required": true, + "schema": { "type": "string", "enum": ["broadcast", "sequence"] } + }, + { + "name": "offset", + "in": "query", + "schema": { "type": "integer", "minimum": 1 } + }, + { + "name": "itemsPerPage", + "in": "query", + "schema": { "type": "integer", "minimum": 1, "maximum": 50 } + } + ], + "responses": { + "200": { + "description": "200", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "type": "object", + "properties": { + "sequenceId": { "type": "string" }, + "type": { "type": "string" }, + "title": { "type": "string" }, + "status": { "type": "string" }, + "deliverySource": { + "discriminator": { "propertyName": "type" }, + "oneOf": [ + { + "type": "object", + "properties": { + "type": { "type": "string", "enum": ["organization"] } + }, + "required": ["type"] + }, + { + "type": "object", + "properties": { + "type": { "type": "string", "enum": ["team"] }, + "espId": { "type": "string", "minLength": 1 } + }, + "required": ["type"] + } + ], + "nullable": true + }, + "triggerType": { "type": "string", "nullable": true }, + "triggerData": { "type": "string", "nullable": true }, + "filter": { "nullable": true }, + "excludeFilter": { "nullable": true }, + "emailsOrder": { + "type": "array", + "items": { "type": "string" } + }, + "entrants": { + "type": "array", + "items": { "type": "string" } + }, + "report": { "nullable": true }, + "createdAt": { "type": "string", "nullable": true }, + "updatedAt": { "type": "string", "nullable": true }, + "emails": { + "type": "array", + "items": { + "type": "object", + "properties": { + "emailId": { "type": "string" }, + "subject": { "type": "string" }, + "content": { + "type": "object", + "properties": { + "style": { + "type": "object", + "additionalProperties": { "nullable": true } + }, + "meta": { + "type": "object", + "additionalProperties": { "nullable": true } + }, + "content": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": { "nullable": true } + } + } + }, + "required": ["style", "meta", "content"] + }, + "delayInMillis": { "type": "number" }, + "published": { "type": "boolean" }, + "templateId": { "type": "string", "nullable": true }, + "actionType": { "type": "string", "nullable": true }, + "actionData": { + "type": "object", + "additionalProperties": { "nullable": true }, + "nullable": true + }, + "createdAt": { "type": "string", "nullable": true }, + "updatedAt": { "type": "string", "nullable": true } + }, + "required": [ + "emailId", + "subject", + "content", + "delayInMillis", + "published", + "createdAt", + "updatedAt" + ] + } + } + }, + "required": [ + "sequenceId", + "type", + "title", + "status", + "deliverySource", + "emailsOrder", + "entrants", + "createdAt", + "updatedAt", + "emails" + ] + } + }, + "total": { "type": "number" } + }, + "required": ["items", "total"] + } + } + } + } + } + } + }, + "/sequences/{sequenceId}": { + "get": { + "summary": "Get a broadcast or sequence", + "tags": ["Sequences"], + "parameters": [ + { + "name": "sequenceId", + "in": "path", + "required": true, + "schema": { "type": "string" } + } + ], + "responses": { + "200": { + "description": "200", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "sequenceId": { "type": "string" }, + "type": { "type": "string" }, + "title": { "type": "string" }, + "status": { "type": "string" }, + "deliverySource": { + "discriminator": { "propertyName": "type" }, + "oneOf": [ + { + "type": "object", + "properties": { + "type": { "type": "string", "enum": ["organization"] } + }, + "required": ["type"] + }, + { + "type": "object", + "properties": { + "type": { "type": "string", "enum": ["team"] }, + "espId": { "type": "string", "minLength": 1 } + }, + "required": ["type"] + } + ], + "nullable": true + }, + "triggerType": { "type": "string", "nullable": true }, + "triggerData": { "type": "string", "nullable": true }, + "filter": { "nullable": true }, + "excludeFilter": { "nullable": true }, + "emailsOrder": { "type": "array", "items": { "type": "string" } }, + "entrants": { "type": "array", "items": { "type": "string" } }, + "report": { "nullable": true }, + "createdAt": { "type": "string", "nullable": true }, + "updatedAt": { "type": "string", "nullable": true }, + "emails": { + "type": "array", + "items": { + "type": "object", + "properties": { + "emailId": { "type": "string" }, + "subject": { "type": "string" }, + "content": { + "type": "object", + "properties": { + "style": { + "type": "object", + "additionalProperties": { "nullable": true } + }, + "meta": { + "type": "object", + "additionalProperties": { "nullable": true } + }, + "content": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": { "nullable": true } + } + } + }, + "required": ["style", "meta", "content"] + }, + "delayInMillis": { "type": "number" }, + "published": { "type": "boolean" }, + "templateId": { "type": "string", "nullable": true }, + "actionType": { "type": "string", "nullable": true }, + "actionData": { + "type": "object", + "additionalProperties": { "nullable": true }, + "nullable": true + }, + "createdAt": { "type": "string", "nullable": true }, + "updatedAt": { "type": "string", "nullable": true } + }, + "required": [ + "emailId", + "subject", + "content", + "delayInMillis", + "published", + "createdAt", + "updatedAt" + ] + } + } + }, + "required": [ + "sequenceId", + "type", + "title", + "status", + "deliverySource", + "emailsOrder", + "entrants", + "createdAt", + "updatedAt", + "emails" + ] + } + } + } + }, + "404": { + "description": "404", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + } + } + }, + "patch": { + "summary": "Update a broadcast or sequence", + "tags": ["Sequences"], + "parameters": [ + { + "name": "sequenceId", + "in": "path", + "required": true, + "schema": { "type": "string" } + } + ], + "requestBody": { + "description": "Body", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "title": { "type": "string" }, + "triggerType": { "type": "string" }, + "triggerData": { "type": "string" }, + "filter": { + "type": "object", + "properties": { + "aggregator": { "type": "string", "enum": ["and", "or"] }, + "filters": { + "type": "array", + "items": { + "oneOf": [ + { + "type": "object", + "properties": { + "name": { "type": "string", "enum": ["email"] }, + "condition": { + "type": "string", + "enum": ["is", "contains", "not_contains"] + }, + "value": { "type": "string" }, + "valueLabel": { "type": "string" } + }, + "required": ["name", "condition", "value"] + }, + { + "type": "object", + "properties": { + "name": { "type": "string", "enum": ["tag"] }, + "condition": { + "type": "string", + "enum": ["is", "is_not"] + }, + "value": { "type": "string" }, + "valueLabel": { "type": "string" } + }, + "required": ["name", "condition", "value"] + }, + { + "type": "object", + "properties": { + "name": { "type": "string", "enum": ["subscription"] }, + "condition": { "type": "string", "enum": ["is"] }, + "value": { + "type": "string", + "enum": ["subscribed", "unsubscribed"] + }, + "valueLabel": { "type": "string" } + }, + "required": ["name", "condition", "value"] + }, + { + "type": "object", + "properties": { + "name": { "type": "string", "enum": ["signedUp"] }, + "condition": { + "type": "string", + "enum": ["before", "after", "on"] + }, + "value": { "type": "string" }, + "valueLabel": { "type": "string" } + }, + "required": ["name", "condition", "value"] + }, + { + "type": "object", + "properties": { + "name": { "type": "string", "enum": ["customField"] }, + "key": { "type": "string", "minLength": 1 }, + "condition": { + "type": "string", + "enum": [ + "is", + "is_not", + "contains", + "not_contains", + "has", + "not_has", + "before", + "after", + "on", + "exists", + "not_exists" + ] + }, + "value": { "type": "string" }, + "valueLabel": { "type": "string" } + }, + "required": ["name", "key", "condition"] + } + ] + } + } + }, + "required": ["aggregator", "filters"] + }, + "emailsOrder": { "type": "array", "items": { "type": "string" } }, + "deliverySource": { + "discriminator": { "propertyName": "type" }, + "oneOf": [ + { + "type": "object", + "properties": { + "type": { "type": "string", "enum": ["organization"] } + }, + "required": ["type"] + }, + { + "type": "object", + "properties": { + "type": { "type": "string", "enum": ["team"] }, + "espId": { "type": "string", "minLength": 1 } + }, + "required": ["type"] + } + ], + "nullable": true + } + } + } + } + } + }, + "responses": { + "200": { + "description": "200", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "sequenceId": { "type": "string" }, + "type": { "type": "string" }, + "title": { "type": "string" }, + "status": { "type": "string" }, + "deliverySource": { + "discriminator": { "propertyName": "type" }, + "oneOf": [ + { + "type": "object", + "properties": { + "type": { "type": "string", "enum": ["organization"] } + }, + "required": ["type"] + }, + { + "type": "object", + "properties": { + "type": { "type": "string", "enum": ["team"] }, + "espId": { "type": "string", "minLength": 1 } + }, + "required": ["type"] + } + ], + "nullable": true + }, + "triggerType": { "type": "string", "nullable": true }, + "triggerData": { "type": "string", "nullable": true }, + "filter": { "nullable": true }, + "excludeFilter": { "nullable": true }, + "emailsOrder": { "type": "array", "items": { "type": "string" } }, + "entrants": { "type": "array", "items": { "type": "string" } }, + "report": { "nullable": true }, + "createdAt": { "type": "string", "nullable": true }, + "updatedAt": { "type": "string", "nullable": true }, + "emails": { + "type": "array", + "items": { + "type": "object", + "properties": { + "emailId": { "type": "string" }, + "subject": { "type": "string" }, + "content": { + "type": "object", + "properties": { + "style": { + "type": "object", + "additionalProperties": { "nullable": true } + }, + "meta": { + "type": "object", + "additionalProperties": { "nullable": true } + }, + "content": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": { "nullable": true } + } + } + }, + "required": ["style", "meta", "content"] + }, + "delayInMillis": { "type": "number" }, + "published": { "type": "boolean" }, + "templateId": { "type": "string", "nullable": true }, + "actionType": { "type": "string", "nullable": true }, + "actionData": { + "type": "object", + "additionalProperties": { "nullable": true }, + "nullable": true + }, + "createdAt": { "type": "string", "nullable": true }, + "updatedAt": { "type": "string", "nullable": true } + }, + "required": [ + "emailId", + "subject", + "content", + "delayInMillis", + "published", + "createdAt", + "updatedAt" + ] + } + } + }, + "required": [ + "sequenceId", + "type", + "title", + "status", + "deliverySource", + "emailsOrder", + "entrants", + "createdAt", + "updatedAt", + "emails" + ] + } + } + } + }, + "400": { + "description": "400", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + }, + "404": { + "description": "404", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + } + } + }, + "delete": { + "summary": "Delete a broadcast or sequence", + "tags": ["Sequences"], + "parameters": [ + { + "name": "sequenceId", + "in": "path", + "required": true, + "schema": { "type": "string" } + } + ], + "responses": { + "204": { "description": "204" }, + "404": { + "description": "404", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + } + } + } + }, + "/sequences/{sequenceId}/emails": { + "post": { + "description": "templateId must identify a marketing template; transactional templates return 422 template_not_marketing.", + "summary": "Add an email to a sequence", + "tags": ["Sequences"], + "parameters": [ + { + "name": "sequenceId", + "in": "path", + "required": true, + "schema": { "type": "string" } + } + ], + "requestBody": { + "description": "Body", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "templateId": { "type": "string", "minLength": 1 } }, + "required": ["templateId"] + } + } + } + }, + "responses": { + "201": { + "description": "201", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "sequenceId": { "type": "string" }, + "type": { "type": "string" }, + "title": { "type": "string" }, + "status": { "type": "string" }, + "deliverySource": { + "discriminator": { "propertyName": "type" }, + "oneOf": [ + { + "type": "object", + "properties": { + "type": { "type": "string", "enum": ["organization"] } + }, + "required": ["type"] + }, + { + "type": "object", + "properties": { + "type": { "type": "string", "enum": ["team"] }, + "espId": { "type": "string", "minLength": 1 } + }, + "required": ["type"] + } + ], + "nullable": true + }, + "triggerType": { "type": "string", "nullable": true }, + "triggerData": { "type": "string", "nullable": true }, + "filter": { "nullable": true }, + "excludeFilter": { "nullable": true }, + "emailsOrder": { "type": "array", "items": { "type": "string" } }, + "entrants": { "type": "array", "items": { "type": "string" } }, + "report": { "nullable": true }, + "createdAt": { "type": "string", "nullable": true }, + "updatedAt": { "type": "string", "nullable": true }, + "emails": { + "type": "array", + "items": { + "type": "object", + "properties": { + "emailId": { "type": "string" }, + "subject": { "type": "string" }, + "content": { + "type": "object", + "properties": { + "style": { + "type": "object", + "additionalProperties": { "nullable": true } + }, + "meta": { + "type": "object", + "additionalProperties": { "nullable": true } + }, + "content": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": { "nullable": true } + } + } + }, + "required": ["style", "meta", "content"] + }, + "delayInMillis": { "type": "number" }, + "published": { "type": "boolean" }, + "templateId": { "type": "string", "nullable": true }, + "actionType": { "type": "string", "nullable": true }, + "actionData": { + "type": "object", + "additionalProperties": { "nullable": true }, + "nullable": true + }, + "createdAt": { "type": "string", "nullable": true }, + "updatedAt": { "type": "string", "nullable": true } + }, + "required": [ + "emailId", + "subject", + "content", + "delayInMillis", + "published", + "createdAt", + "updatedAt" + ] + } + } + }, + "required": [ + "sequenceId", + "type", + "title", + "status", + "deliverySource", + "emailsOrder", + "entrants", + "createdAt", + "updatedAt", + "emails" + ] + } + } + } + }, + "400": { + "description": "400", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + }, + "404": { + "description": "404", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + }, + "422": { + "description": "422", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { "type": "string", "enum": ["template_not_marketing"] } + }, + "required": ["error"] + } + } + } + } + } + } + }, + "/sequences/{sequenceId}/emails/{emailId}": { + "patch": { + "description": "When templateId is supplied, it must identify a marketing template; transactional templates return 422 template_not_marketing.", + "summary": "Update an email within a sequence", + "tags": ["Sequences"], + "parameters": [ + { + "name": "sequenceId", + "in": "path", + "required": true, + "schema": { "type": "string" } + }, + { + "name": "emailId", + "in": "path", + "required": true, + "schema": { "type": "string" } + } + ], + "requestBody": { + "description": "Body", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "subject": { "type": "string" }, + "content": { + "type": "object", + "properties": { + "style": { + "type": "object", + "properties": { + "colors": { + "type": "object", + "properties": { + "background": { + "type": "string", + "pattern": "^#(?:[\\da-f]{3,4}|[\\da-f]{6}|[\\da-f]{8})$" + }, + "foreground": { + "type": "string", + "pattern": "^#(?:[\\da-f]{3,4}|[\\da-f]{6}|[\\da-f]{8})$" + }, + "border": { + "type": "string", + "pattern": "^#(?:[\\da-f]{3,4}|[\\da-f]{6}|[\\da-f]{8})$" + }, + "accent": { + "type": "string", + "pattern": "^#(?:[\\da-f]{3,4}|[\\da-f]{6}|[\\da-f]{8})$" + }, + "accentForeground": { + "type": "string", + "pattern": "^#(?:[\\da-f]{3,4}|[\\da-f]{6}|[\\da-f]{8})$" + } + }, + "required": [ + "background", + "foreground", + "border", + "accent", + "accentForeground" + ], + "additionalProperties": false + }, + "typography": { + "type": "object", + "properties": { + "header": { + "type": "object", + "properties": {}, + "additionalProperties": true + }, + "text": { + "type": "object", + "properties": {}, + "additionalProperties": true + }, + "link": { + "type": "object", + "properties": {}, + "additionalProperties": true + } + }, + "required": ["header", "text", "link"], + "additionalProperties": false + }, + "interactives": { + "type": "object", + "properties": { + "button": { + "type": "object", + "properties": { + "padding": { + "type": "object", + "properties": { + "x": { + "type": "string", + "pattern": "^\\d+(?:\\.\\d+)?px$" + }, + "y": { + "type": "string", + "pattern": "^\\d+(?:\\.\\d+)?px$" + } + } + }, + "border": { + "type": "object", + "properties": { + "width": { + "type": "string", + "pattern": "^\\d+(?:\\.\\d+)?px$" + }, + "radius": { + "type": "string", + "pattern": "^\\d+(?:\\.\\d+)?px$" + }, + "style": { "type": "string", "minLength": 1 } + } + } + }, + "additionalProperties": true + }, + "link": { + "type": "object", + "properties": { + "padding": { + "type": "object", + "properties": { + "x": { + "type": "string", + "pattern": "^\\d+(?:\\.\\d+)?px$" + }, + "y": { + "type": "string", + "pattern": "^\\d+(?:\\.\\d+)?px$" + } + } + } + }, + "additionalProperties": true + } + }, + "required": ["button", "link"], + "additionalProperties": false + }, + "structure": { + "type": "object", + "properties": { + "page": { + "type": "object", + "properties": { + "background": { + "type": "string", + "pattern": "^#(?:[\\da-f]{3,4}|[\\da-f]{6}|[\\da-f]{8})$" + }, + "foreground": { + "type": "string", + "pattern": "^#(?:[\\da-f]{3,4}|[\\da-f]{6}|[\\da-f]{8})$" + }, + "width": { + "type": "string", + "pattern": "^\\d+(?:\\.\\d+)?px$" + }, + "marginY": { + "type": "string", + "pattern": "^\\d+(?:\\.\\d+)?px$" + }, + "borderWidth": { + "type": "string", + "pattern": "^\\d+(?:\\.\\d+)?px$" + }, + "borderStyle": { "type": "string", "minLength": 1 }, + "borderRadius": { + "type": "string", + "pattern": "^\\d+(?:\\.\\d+)?px$" + } + }, + "additionalProperties": false + }, + "section": { + "type": "object", + "properties": { + "padding": { + "type": "object", + "properties": { + "x": { + "type": "string", + "pattern": "^\\d+(?:\\.\\d+)?px$" + }, + "y": { + "type": "string", + "pattern": "^\\d+(?:\\.\\d+)?px$" + } + } + } + }, + "additionalProperties": false + } + }, + "required": ["page", "section"], + "additionalProperties": false + } + }, + "required": [ + "colors", + "typography", + "interactives", + "structure" + ], + "additionalProperties": false + }, + "meta": { + "type": "object", + "properties": { + "previewText": { "type": "string" }, + "utm": { + "type": "object", + "properties": { + "source": { "type": "string" }, + "medium": { "type": "string" }, + "campaign": { "type": "string" } + }, + "required": ["source", "medium", "campaign"], + "additionalProperties": false + } + }, + "additionalProperties": false + }, + "content": { + "type": "array", + "items": { + "discriminator": { "propertyName": "blockType" }, + "oneOf": [ + { + "type": "object", + "properties": { + "id": { "type": "string" }, + "settings": { + "type": "object", + "properties": { "content": { "type": "string" } }, + "required": ["content"], + "additionalProperties": true + }, + "blockType": { "type": "string", "enum": ["text"] } + }, + "required": ["settings", "blockType"] + }, + { + "type": "object", + "properties": { + "id": { "type": "string" }, + "settings": { + "type": "object", + "properties": { + "text": { "type": "string" }, + "url": { "type": "string", "minLength": 1 } + }, + "required": ["text", "url"], + "additionalProperties": true + }, + "blockType": { "type": "string", "enum": ["link"] } + }, + "required": ["settings", "blockType"] + }, + { + "type": "object", + "properties": { + "id": { "type": "string" }, + "settings": { + "type": "object", + "properties": { + "src": { "type": "string", "minLength": 1 } + }, + "required": ["src"], + "additionalProperties": true + }, + "blockType": { "type": "string", "enum": ["image"] } + }, + "required": ["settings", "blockType"] + }, + { + "type": "object", + "properties": { + "id": { "type": "string" }, + "settings": { + "type": "object", + "additionalProperties": { "nullable": true } + }, + "blockType": { "type": "string", "enum": ["separator"] } + }, + "required": ["settings", "blockType"] + }, + { + "type": "object", + "properties": { + "id": { "type": "string" }, + "settings": { + "type": "object", + "additionalProperties": { "nullable": true } + }, + "blockType": { "type": "string", "enum": ["footer"] } + }, + "required": ["settings", "blockType"] + } + ] + } + } + }, + "required": ["style", "meta", "content"] + }, + "delayInMillis": { "type": "number" }, + "templateId": { "type": "string", "minLength": 1 }, + "actionType": { "type": "string", "enum": ["tag:add", "tag:remove"] }, + "actionData": { + "type": "object", + "additionalProperties": { "nullable": true } + }, + "published": { "type": "boolean" } + } + } + } + } + }, + "responses": { + "200": { + "description": "200", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "sequenceId": { "type": "string" }, + "type": { "type": "string" }, + "title": { "type": "string" }, + "status": { "type": "string" }, + "deliverySource": { + "discriminator": { "propertyName": "type" }, + "oneOf": [ + { + "type": "object", + "properties": { + "type": { "type": "string", "enum": ["organization"] } + }, + "required": ["type"] + }, + { + "type": "object", + "properties": { + "type": { "type": "string", "enum": ["team"] }, + "espId": { "type": "string", "minLength": 1 } + }, + "required": ["type"] + } + ], + "nullable": true + }, + "triggerType": { "type": "string", "nullable": true }, + "triggerData": { "type": "string", "nullable": true }, + "filter": { "nullable": true }, + "excludeFilter": { "nullable": true }, + "emailsOrder": { "type": "array", "items": { "type": "string" } }, + "entrants": { "type": "array", "items": { "type": "string" } }, + "report": { "nullable": true }, + "createdAt": { "type": "string", "nullable": true }, + "updatedAt": { "type": "string", "nullable": true }, + "emails": { + "type": "array", + "items": { + "type": "object", + "properties": { + "emailId": { "type": "string" }, + "subject": { "type": "string" }, + "content": { + "type": "object", + "properties": { + "style": { + "type": "object", + "additionalProperties": { "nullable": true } + }, + "meta": { + "type": "object", + "additionalProperties": { "nullable": true } + }, + "content": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": { "nullable": true } + } + } + }, + "required": ["style", "meta", "content"] + }, + "delayInMillis": { "type": "number" }, + "published": { "type": "boolean" }, + "templateId": { "type": "string", "nullable": true }, + "actionType": { "type": "string", "nullable": true }, + "actionData": { + "type": "object", + "additionalProperties": { "nullable": true }, + "nullable": true + }, + "createdAt": { "type": "string", "nullable": true }, + "updatedAt": { "type": "string", "nullable": true } + }, + "required": [ + "emailId", + "subject", + "content", + "delayInMillis", + "published", + "createdAt", + "updatedAt" + ] + } + } + }, + "required": [ + "sequenceId", + "type", + "title", + "status", + "deliverySource", + "emailsOrder", + "entrants", + "createdAt", + "updatedAt", + "emails" + ] + } + } + } + }, + "400": { + "description": "400", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + }, + "404": { + "description": "404", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + }, + "422": { + "description": "422", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { "type": "string", "enum": ["template_not_marketing"] } + }, + "required": ["error"] + } + } + } + } + } + }, + "delete": { + "summary": "Remove an email from a sequence", + "tags": ["Sequences"], + "parameters": [ + { + "name": "sequenceId", + "in": "path", + "required": true, + "schema": { "type": "string" } + }, + { + "name": "emailId", + "in": "path", + "required": true, + "schema": { "type": "string" } + } + ], + "responses": { + "200": { + "description": "200", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "sequenceId": { "type": "string" }, + "type": { "type": "string" }, + "title": { "type": "string" }, + "status": { "type": "string" }, + "deliverySource": { + "discriminator": { "propertyName": "type" }, + "oneOf": [ + { + "type": "object", + "properties": { + "type": { "type": "string", "enum": ["organization"] } + }, + "required": ["type"] + }, + { + "type": "object", + "properties": { + "type": { "type": "string", "enum": ["team"] }, + "espId": { "type": "string", "minLength": 1 } + }, + "required": ["type"] + } + ], + "nullable": true + }, + "triggerType": { "type": "string", "nullable": true }, + "triggerData": { "type": "string", "nullable": true }, + "filter": { "nullable": true }, + "excludeFilter": { "nullable": true }, + "emailsOrder": { "type": "array", "items": { "type": "string" } }, + "entrants": { "type": "array", "items": { "type": "string" } }, + "report": { "nullable": true }, + "createdAt": { "type": "string", "nullable": true }, + "updatedAt": { "type": "string", "nullable": true }, + "emails": { + "type": "array", + "items": { + "type": "object", + "properties": { + "emailId": { "type": "string" }, + "subject": { "type": "string" }, + "content": { + "type": "object", + "properties": { + "style": { + "type": "object", + "additionalProperties": { "nullable": true } + }, + "meta": { + "type": "object", + "additionalProperties": { "nullable": true } + }, + "content": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": { "nullable": true } + } + } + }, + "required": ["style", "meta", "content"] + }, + "delayInMillis": { "type": "number" }, + "published": { "type": "boolean" }, + "templateId": { "type": "string", "nullable": true }, + "actionType": { "type": "string", "nullable": true }, + "actionData": { + "type": "object", + "additionalProperties": { "nullable": true }, + "nullable": true + }, + "createdAt": { "type": "string", "nullable": true }, + "updatedAt": { "type": "string", "nullable": true } + }, + "required": [ + "emailId", + "subject", + "content", + "delayInMillis", + "published", + "createdAt", + "updatedAt" + ] + } + } + }, + "required": [ + "sequenceId", + "type", + "title", + "status", + "deliverySource", + "emailsOrder", + "entrants", + "createdAt", + "updatedAt", + "emails" + ] + } + } + } + }, + "400": { + "description": "400", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + }, + "404": { + "description": "404", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + } + } + } + }, + "/sequences/{sequenceId}/start": { + "post": { + "description": "Requires a non-empty mailing address in the team's general settings.", + "summary": "Start a broadcast or activate a sequence", + "tags": ["Sequences"], + "parameters": [ + { + "name": "sequenceId", + "in": "path", + "required": true, + "schema": { "type": "string" } + } + ], + "responses": { + "200": { + "description": "200", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "sequenceId": { "type": "string" }, + "type": { "type": "string" }, + "title": { "type": "string" }, + "status": { "type": "string" }, + "deliverySource": { + "discriminator": { "propertyName": "type" }, + "oneOf": [ + { + "type": "object", + "properties": { + "type": { "type": "string", "enum": ["organization"] } + }, + "required": ["type"] + }, + { + "type": "object", + "properties": { + "type": { "type": "string", "enum": ["team"] }, + "espId": { "type": "string", "minLength": 1 } + }, + "required": ["type"] + } + ], + "nullable": true + }, + "triggerType": { "type": "string", "nullable": true }, + "triggerData": { "type": "string", "nullable": true }, + "filter": { "nullable": true }, + "excludeFilter": { "nullable": true }, + "emailsOrder": { "type": "array", "items": { "type": "string" } }, + "entrants": { "type": "array", "items": { "type": "string" } }, + "report": { "nullable": true }, + "createdAt": { "type": "string", "nullable": true }, + "updatedAt": { "type": "string", "nullable": true }, + "emails": { + "type": "array", + "items": { + "type": "object", + "properties": { + "emailId": { "type": "string" }, + "subject": { "type": "string" }, + "content": { + "type": "object", + "properties": { + "style": { + "type": "object", + "additionalProperties": { "nullable": true } + }, + "meta": { + "type": "object", + "additionalProperties": { "nullable": true } + }, + "content": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": { "nullable": true } + } + } + }, + "required": ["style", "meta", "content"] + }, + "delayInMillis": { "type": "number" }, + "published": { "type": "boolean" }, + "templateId": { "type": "string", "nullable": true }, + "actionType": { "type": "string", "nullable": true }, + "actionData": { + "type": "object", + "additionalProperties": { "nullable": true }, + "nullable": true + }, + "createdAt": { "type": "string", "nullable": true }, + "updatedAt": { "type": "string", "nullable": true } + }, + "required": [ + "emailId", + "subject", + "content", + "delayInMillis", + "published", + "createdAt", + "updatedAt" + ] + } + } + }, + "required": [ + "sequenceId", + "type", + "title", + "status", + "deliverySource", + "emailsOrder", + "entrants", + "createdAt", + "updatedAt", + "emails" + ] + } + } + } + }, + "400": { + "description": "400", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + }, + "422": { + "description": "422", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + } + } + } + }, + "/sequences/{sequenceId}/pause": { + "post": { + "summary": "Pause an active sequence", + "tags": ["Sequences"], + "parameters": [ + { + "name": "sequenceId", + "in": "path", + "required": true, + "schema": { "type": "string" } + } + ], + "responses": { + "200": { + "description": "200", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "sequenceId": { "type": "string" }, + "type": { "type": "string" }, + "title": { "type": "string" }, + "status": { "type": "string" }, + "deliverySource": { + "discriminator": { "propertyName": "type" }, + "oneOf": [ + { + "type": "object", + "properties": { + "type": { "type": "string", "enum": ["organization"] } + }, + "required": ["type"] + }, + { + "type": "object", + "properties": { + "type": { "type": "string", "enum": ["team"] }, + "espId": { "type": "string", "minLength": 1 } + }, + "required": ["type"] + } + ], + "nullable": true + }, + "triggerType": { "type": "string", "nullable": true }, + "triggerData": { "type": "string", "nullable": true }, + "filter": { "nullable": true }, + "excludeFilter": { "nullable": true }, + "emailsOrder": { "type": "array", "items": { "type": "string" } }, + "entrants": { "type": "array", "items": { "type": "string" } }, + "report": { "nullable": true }, + "createdAt": { "type": "string", "nullable": true }, + "updatedAt": { "type": "string", "nullable": true }, + "emails": { + "type": "array", + "items": { + "type": "object", + "properties": { + "emailId": { "type": "string" }, + "subject": { "type": "string" }, + "content": { + "type": "object", + "properties": { + "style": { + "type": "object", + "additionalProperties": { "nullable": true } + }, + "meta": { + "type": "object", + "additionalProperties": { "nullable": true } + }, + "content": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": { "nullable": true } + } + } + }, + "required": ["style", "meta", "content"] + }, + "delayInMillis": { "type": "number" }, + "published": { "type": "boolean" }, + "templateId": { "type": "string", "nullable": true }, + "actionType": { "type": "string", "nullable": true }, + "actionData": { + "type": "object", + "additionalProperties": { "nullable": true }, + "nullable": true + }, + "createdAt": { "type": "string", "nullable": true }, + "updatedAt": { "type": "string", "nullable": true } + }, + "required": [ + "emailId", + "subject", + "content", + "delayInMillis", + "published", + "createdAt", + "updatedAt" + ] + } + } + }, + "required": [ + "sequenceId", + "type", + "title", + "status", + "deliverySource", + "emailsOrder", + "entrants", + "createdAt", + "updatedAt", + "emails" + ] + } + } + } + }, + "400": { + "description": "400", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + } + } + } + }, + "/sequences/{sequenceId}/stats": { + "get": { + "summary": "Delivery / open-rate / click-through-rate stats for a sequence", + "tags": ["Sequences"], + "parameters": [ + { + "name": "sequenceId", + "in": "path", + "required": true, + "schema": { "type": "string" } + } + ], + "responses": { + "200": { + "description": "200", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "sent": { "type": "number" }, + "openRate": { "type": "number" }, + "clickThroughRate": { "type": "number" }, + "subscribersCount": { "type": "number" } + }, + "required": [ + "sent", + "openRate", + "clickThroughRate", + "subscribersCount" + ] + } + } + } + } + } + } + }, + "/sequences/{sequenceId}/subscribers": { + "get": { + "summary": "List contact ids that have received this sequence", + "tags": ["Sequences"], + "parameters": [ + { + "name": "sequenceId", + "in": "path", + "required": true, + "schema": { "type": "string" } + }, + { + "name": "page", + "in": "query", + "schema": { "type": "integer", "minimum": 1 } + }, + { + "name": "limit", + "in": "query", + "schema": { "type": "integer", "minimum": 1 } + } + ], + "responses": { "200": { "description": "200" } } + } + }, + "/emails": { + "post": { + "description": "Requires a non-empty mailing address in the team's general settings. `templateId` must identify a team-owned transactional template; marketing templates return 422 template_not_transactional. Unguarded Liquid variables must be supplied in `variables`, or the API returns 422 missing_template_variables without creating a row or queue job. Inline `html` is sent verbatim without Liquid rendering. address, unsubscribe_link, and subscriber are marketing-only server-owned values and are unavailable to transactional templates.", + "summary": "Send a transactional email", + "tags": ["Transactional Emails"], + "parameters": [], + "requestBody": { + "description": "Body", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "to": { "type": "string", "format": "email" }, + "subject": { "type": "string", "minLength": 1 }, + "templateId": { "type": "string", "minLength": 1 }, + "html": { "type": "string", "minLength": 1 }, + "variables": { + "type": "object", + "additionalProperties": { "nullable": true } + }, + "replyTo": { "type": "string", "format": "email" }, + "headers": { + "type": "object", + "additionalProperties": { "type": "string" } + }, + "idempotencyKey": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "trackOpens": { "type": "boolean" }, + "trackClicks": { "type": "boolean" }, + "deliverySource": { + "discriminator": { "propertyName": "type" }, + "oneOf": [ + { + "type": "object", + "properties": { + "type": { "type": "string", "enum": ["organization"] } + }, + "required": ["type"] + }, + { + "type": "object", + "properties": { + "type": { "type": "string", "enum": ["team"] }, + "espId": { "type": "string", "minLength": 1 } + }, + "required": ["type"] + } + ] + } + }, + "required": ["to", "subject"] + } + } + } + }, + "responses": { + "202": { + "description": "202", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "txeId": { "type": "string" }, + "status": { + "type": "string", + "enum": ["queued", "sent", "failed", "bounced", "suppressed"] + } + }, + "required": ["txeId", "status"] + } + } + } + }, + "400": { + "description": "400", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + }, + "402": { + "description": "402", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + }, + "409": { + "description": "409", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + }, + "422": { + "description": "422", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "type": "object", + "properties": { + "error": { + "type": "string", + "enum": ["missing_template_variables"] + }, + "missingVariables": { + "type": "array", + "items": { "type": "string" }, + "minItems": 1 + } + }, + "required": ["error", "missingVariables"] + }, + { + "type": "object", + "properties": { + "error": { + "type": "string", + "enum": ["template_not_transactional"] + } + }, + "required": ["error"] + }, + { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + ] + } + } + } + }, + "429": { + "description": "429", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + } + } + }, + "get": { + "summary": "List transactional emails", + "tags": ["Transactional Emails"], + "parameters": [ + { + "name": "status", + "in": "query", + "schema": { + "type": "string", + "enum": ["queued", "sent", "failed", "bounced", "suppressed"] + } + }, + { + "name": "createdAfter", + "in": "query", + "description": "Millisecond timestamp lower bound (inclusive)", + "schema": { "type": "integer", "nullable": true } + }, + { + "name": "createdBefore", + "in": "query", + "description": "Millisecond timestamp upper bound (exclusive)", + "schema": { "type": "integer", "nullable": true } + }, + { + "name": "offset", + "in": "query", + "schema": { "type": "integer", "minimum": 1 } + }, + { + "name": "itemsPerPage", + "in": "query", + "schema": { "type": "integer", "minimum": 1, "maximum": 50 } + } + ], + "responses": { + "200": { + "description": "200", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "type": "object", + "properties": { + "txeId": { "type": "string" }, + "to": { "type": "string" }, + "from": { "type": "string", "nullable": true }, + "replyTo": { "type": "string", "nullable": true }, + "subject": { "type": "string" }, + "templateId": { "type": "string", "nullable": true }, + "variables": { + "type": "object", + "additionalProperties": { "nullable": true } + }, + "status": { + "type": "string", + "enum": [ + "queued", + "sent", + "failed", + "bounced", + "suppressed" + ] + }, + "error": { "type": "string", "nullable": true }, + "trackOpens": { "type": "boolean" }, + "trackClicks": { "type": "boolean" }, + "openCount": { "type": "number" }, + "clickCount": { "type": "number" }, + "sentAt": { "type": "string", "nullable": true }, + "createdAt": { "type": "string", "nullable": true }, + "updatedAt": { "type": "string", "nullable": true } + }, + "required": [ + "txeId", + "to", + "from", + "replyTo", + "subject", + "templateId", + "variables", + "status", + "error", + "trackOpens", + "trackClicks", + "openCount", + "clickCount", + "sentAt", + "createdAt", + "updatedAt" + ] + } + }, + "total": { "type": "number" } + }, + "required": ["items", "total"] + } + } + } + } + } + } + }, + "/emails/{txeId}": { + "get": { + "summary": "Get a transactional email", + "tags": ["Transactional Emails"], + "parameters": [ + { + "name": "txeId", + "in": "path", + "required": true, + "schema": { "type": "string" } + } + ], + "responses": { + "200": { + "description": "200", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "txeId": { "type": "string" }, + "to": { "type": "string" }, + "from": { "type": "string", "nullable": true }, + "replyTo": { "type": "string", "nullable": true }, + "subject": { "type": "string" }, + "templateId": { "type": "string", "nullable": true }, + "variables": { + "type": "object", + "additionalProperties": { "nullable": true } + }, + "status": { + "type": "string", + "enum": ["queued", "sent", "failed", "bounced", "suppressed"] + }, + "error": { "type": "string", "nullable": true }, + "trackOpens": { "type": "boolean" }, + "trackClicks": { "type": "boolean" }, + "openCount": { "type": "number" }, + "clickCount": { "type": "number" }, + "sentAt": { "type": "string", "nullable": true }, + "createdAt": { "type": "string", "nullable": true }, + "updatedAt": { "type": "string", "nullable": true }, + "html": { "type": "string", "nullable": true } + }, + "required": [ + "txeId", + "to", + "from", + "replyTo", + "subject", + "templateId", + "variables", + "status", + "error", + "trackOpens", + "trackClicks", + "openCount", + "clickCount", + "sentAt", + "createdAt", + "updatedAt", + "html" + ] + } + } + } + }, + "404": { + "description": "404", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + } + } + } + }, + "/settings/esps": { + "get": { + "summary": "List the team's user-managed ESP configurations", + "tags": ["Settings"], + "parameters": [], + "responses": { + "200": { + "description": "200", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "type": "object", + "properties": { + "espId": { "type": "string" }, + "name": { "type": "string" }, + "provider": { "type": "string" }, + "host": { "type": "string" }, + "port": { "type": "number" }, + "secure": { "type": "boolean" }, + "username": { "type": "string", "nullable": true }, + "hasPassword": { "type": "boolean" }, + "fromName": { "type": "string", "nullable": true }, + "fromEmail": { "type": "string", "nullable": true }, + "status": { + "type": "string", + "enum": [ + "draft", + "active", + "suspended", + "draining", + "retired" + ] + }, + "secretVersion": { + "type": "integer", + "minimum": 0, + "exclusiveMinimum": 0 + }, + "lastTestedAt": { "type": "string", "nullable": true }, + "lastTestStatus": { + "type": "string", + "enum": ["success", "failed"], + "nullable": true + }, + "lastTestError": { "type": "string", "nullable": true }, + "activatedAt": { "type": "string", "nullable": true }, + "drainUntil": { "type": "string", "nullable": true }, + "retiredAt": { "type": "string", "nullable": true }, + "updatedAt": { "type": "string" } + }, + "required": [ + "espId", + "name", + "provider", + "host", + "port", + "secure", + "hasPassword", + "status", + "secretVersion" + ] + } + } + }, + "required": ["items"] + } + } + } + } + } + }, + "post": { + "summary": "Create a user-managed ESP configuration", + "tags": ["Settings"], + "parameters": [], + "requestBody": { + "description": "Body", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "provider": { + "type": "string", + "enum": ["smtp", "sendgrid", "mailgun", "postmark", "ses", "resend"] + }, + "host": { "type": "string", "minLength": 1 }, + "port": { "type": "integer", "minimum": 1, "maximum": 65535 }, + "secure": { "type": "boolean" }, + "username": { "type": "string" }, + "password": { "type": "string" }, + "fromName": { "type": "string" }, + "fromEmail": { "type": "string", "format": "email" }, + "name": { "type": "string", "minLength": 1, "maxLength": 100 } + }, + "required": ["provider", "host", "port", "secure", "name"] + } + } + } + }, + "responses": { + "201": { + "description": "201", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "espId": { "type": "string" }, + "name": { "type": "string" }, + "provider": { "type": "string" }, + "host": { "type": "string" }, + "port": { "type": "number" }, + "secure": { "type": "boolean" }, + "username": { "type": "string", "nullable": true }, + "hasPassword": { "type": "boolean" }, + "fromName": { "type": "string", "nullable": true }, + "fromEmail": { "type": "string", "nullable": true }, + "status": { + "type": "string", + "enum": ["draft", "active", "suspended", "draining", "retired"] + }, + "secretVersion": { + "type": "integer", + "minimum": 0, + "exclusiveMinimum": 0 + }, + "lastTestedAt": { "type": "string", "nullable": true }, + "lastTestStatus": { + "type": "string", + "enum": ["success", "failed"], + "nullable": true + }, + "lastTestError": { "type": "string", "nullable": true }, + "activatedAt": { "type": "string", "nullable": true }, + "drainUntil": { "type": "string", "nullable": true }, + "retiredAt": { "type": "string", "nullable": true }, + "updatedAt": { "type": "string" } + }, + "required": [ + "espId", + "name", + "provider", + "host", + "port", + "secure", + "hasPassword", + "status", + "secretVersion" + ] + } + } + } + }, + "403": { + "description": "403", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + } + } + } + }, + "/settings/esps/{espId}": { + "get": { + "summary": "Get a user-managed ESP configuration", + "tags": ["Settings"], + "parameters": [ + { + "name": "espId", + "in": "path", + "required": true, + "schema": { "type": "string" } + } + ], + "responses": { + "200": { + "description": "200", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "espId": { "type": "string" }, + "name": { "type": "string" }, + "provider": { "type": "string" }, + "host": { "type": "string" }, + "port": { "type": "number" }, + "secure": { "type": "boolean" }, + "username": { "type": "string", "nullable": true }, + "hasPassword": { "type": "boolean" }, + "fromName": { "type": "string", "nullable": true }, + "fromEmail": { "type": "string", "nullable": true }, + "status": { + "type": "string", + "enum": ["draft", "active", "suspended", "draining", "retired"] + }, + "secretVersion": { + "type": "integer", + "minimum": 0, + "exclusiveMinimum": 0 + }, + "lastTestedAt": { "type": "string", "nullable": true }, + "lastTestStatus": { + "type": "string", + "enum": ["success", "failed"], + "nullable": true + }, + "lastTestError": { "type": "string", "nullable": true }, + "activatedAt": { "type": "string", "nullable": true }, + "drainUntil": { "type": "string", "nullable": true }, + "retiredAt": { "type": "string", "nullable": true }, + "updatedAt": { "type": "string" } + }, + "required": [ + "espId", + "name", + "provider", + "host", + "port", + "secure", + "hasPassword", + "status", + "secretVersion" + ] + } + } + } + }, + "403": { + "description": "403", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + }, + "404": { + "description": "404", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + }, + "409": { + "description": "409", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + } + } + }, + "patch": { + "summary": "Update a user-managed ESP configuration", + "tags": ["Settings"], + "parameters": [ + { + "name": "espId", + "in": "path", + "required": true, + "schema": { "type": "string" } + } + ], + "requestBody": { + "description": "Body", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "name": { "type": "string", "minLength": 1, "maxLength": 100 }, + "provider": { + "type": "string", + "enum": ["smtp", "sendgrid", "mailgun", "postmark", "ses", "resend"] + }, + "host": { "type": "string", "minLength": 1 }, + "port": { "type": "integer", "minimum": 1, "maximum": 65535 }, + "secure": { "type": "boolean" }, + "username": { "type": "string" }, + "password": { "type": "string" }, + "fromName": { "type": "string" }, + "fromEmail": { + "oneOf": [ + { "type": "string", "format": "email" }, + { "type": "string", "enum": [""] } + ] + } + } + } + } + } + }, + "responses": { + "200": { + "description": "200", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "espId": { "type": "string" }, + "name": { "type": "string" }, + "provider": { "type": "string" }, + "host": { "type": "string" }, + "port": { "type": "number" }, + "secure": { "type": "boolean" }, + "username": { "type": "string", "nullable": true }, + "hasPassword": { "type": "boolean" }, + "fromName": { "type": "string", "nullable": true }, + "fromEmail": { "type": "string", "nullable": true }, + "status": { + "type": "string", + "enum": ["draft", "active", "suspended", "draining", "retired"] + }, + "secretVersion": { + "type": "integer", + "minimum": 0, + "exclusiveMinimum": 0 + }, + "lastTestedAt": { "type": "string", "nullable": true }, + "lastTestStatus": { + "type": "string", + "enum": ["success", "failed"], + "nullable": true + }, + "lastTestError": { "type": "string", "nullable": true }, + "activatedAt": { "type": "string", "nullable": true }, + "drainUntil": { "type": "string", "nullable": true }, + "retiredAt": { "type": "string", "nullable": true }, + "updatedAt": { "type": "string" } + }, + "required": [ + "espId", + "name", + "provider", + "host", + "port", + "secure", + "hasPassword", + "status", + "secretVersion" + ] + } + } + } + }, + "403": { + "description": "403", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + }, + "404": { + "description": "404", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + }, + "409": { + "description": "409", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + } + } + }, + "delete": { + "description": "Removes an ESP with no active or queued dependencies. Draft, paused, and completed sequences are detached first.", + "summary": "Remove a user-managed ESP configuration", + "tags": ["Settings"], + "parameters": [ + { + "name": "espId", + "in": "path", + "required": true, + "schema": { "type": "string" } + } + ], + "responses": { + "204": { "description": "204" }, + "403": { + "description": "403", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + }, + "404": { + "description": "404", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + }, + "409": { + "description": "409", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + } + } + } + }, + "/settings/esps/{espId}/test": { + "post": { + "description": "Requires a non-empty mailing address in the team's general settings.", + "summary": "Send a test email through a user-managed ESP", + "tags": ["Settings"], + "parameters": [ + { + "name": "espId", + "in": "path", + "required": true, + "schema": { "type": "string" } + } + ], + "requestBody": { + "description": "Body", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "to": { "type": "string", "format": "email" } } + } + } + } + }, + "responses": { + "200": { + "description": "200", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { "type": "boolean" }, + "error": { "type": "string" } + }, + "required": ["success"] + } + } + } + }, + "400": { + "description": "400", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + }, + "403": { + "description": "403", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + }, + "404": { + "description": "404", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + }, + "422": { + "description": "422", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + }, + "502": { + "description": "502", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { "type": "boolean" }, + "error": { "type": "string" } + }, + "required": ["success"] + } + } + } + } + } + } + }, + "/settings/esps/{espId}/activate": { + "post": { + "summary": "Activate a verified team ESP", + "tags": ["Settings"], + "parameters": [ + { + "name": "espId", + "in": "path", + "required": true, + "schema": { "type": "string" } + } + ], + "responses": { + "200": { + "description": "200", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "espId": { "type": "string" }, + "name": { "type": "string" }, + "provider": { "type": "string" }, + "host": { "type": "string" }, + "port": { "type": "number" }, + "secure": { "type": "boolean" }, + "username": { "type": "string", "nullable": true }, + "hasPassword": { "type": "boolean" }, + "fromName": { "type": "string", "nullable": true }, + "fromEmail": { "type": "string", "nullable": true }, + "status": { + "type": "string", + "enum": ["draft", "active", "suspended", "draining", "retired"] + }, + "secretVersion": { + "type": "integer", + "minimum": 0, + "exclusiveMinimum": 0 + }, + "lastTestedAt": { "type": "string", "nullable": true }, + "lastTestStatus": { + "type": "string", + "enum": ["success", "failed"], + "nullable": true + }, + "lastTestError": { "type": "string", "nullable": true }, + "activatedAt": { "type": "string", "nullable": true }, + "drainUntil": { "type": "string", "nullable": true }, + "retiredAt": { "type": "string", "nullable": true }, + "updatedAt": { "type": "string" } + }, + "required": [ + "espId", + "name", + "provider", + "host", + "port", + "secure", + "hasPassword", + "status", + "secretVersion" + ] + } + } + } + }, + "403": { + "description": "403", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + }, + "404": { + "description": "404", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + }, + "409": { + "description": "409", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + }, + "422": { + "description": "422", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + } + } + } + }, + "/settings/esps/{espId}/suspend": { + "post": { + "summary": "Suspend a team ESP", + "tags": ["Settings"], + "parameters": [ + { + "name": "espId", + "in": "path", + "required": true, + "schema": { "type": "string" } + } + ], + "responses": { + "200": { + "description": "200", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "espId": { "type": "string" }, + "name": { "type": "string" }, + "provider": { "type": "string" }, + "host": { "type": "string" }, + "port": { "type": "number" }, + "secure": { "type": "boolean" }, + "username": { "type": "string", "nullable": true }, + "hasPassword": { "type": "boolean" }, + "fromName": { "type": "string", "nullable": true }, + "fromEmail": { "type": "string", "nullable": true }, + "status": { + "type": "string", + "enum": ["draft", "active", "suspended", "draining", "retired"] + }, + "secretVersion": { + "type": "integer", + "minimum": 0, + "exclusiveMinimum": 0 + }, + "lastTestedAt": { "type": "string", "nullable": true }, + "lastTestStatus": { + "type": "string", + "enum": ["success", "failed"], + "nullable": true + }, + "lastTestError": { "type": "string", "nullable": true }, + "activatedAt": { "type": "string", "nullable": true }, + "drainUntil": { "type": "string", "nullable": true }, + "retiredAt": { "type": "string", "nullable": true }, + "updatedAt": { "type": "string" } + }, + "required": [ + "espId", + "name", + "provider", + "host", + "port", + "secure", + "hasPassword", + "status", + "secretVersion" + ] + } + } + } + }, + "403": { + "description": "403", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + }, + "404": { + "description": "404", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + }, + "409": { + "description": "409", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + } + } + } + }, + "/settings/esps/{espId}/resume": { + "post": { + "summary": "Resume a team ESP", + "tags": ["Settings"], + "parameters": [ + { + "name": "espId", + "in": "path", + "required": true, + "schema": { "type": "string" } + } + ], + "responses": { + "200": { + "description": "200", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "espId": { "type": "string" }, + "name": { "type": "string" }, + "provider": { "type": "string" }, + "host": { "type": "string" }, + "port": { "type": "number" }, + "secure": { "type": "boolean" }, + "username": { "type": "string", "nullable": true }, + "hasPassword": { "type": "boolean" }, + "fromName": { "type": "string", "nullable": true }, + "fromEmail": { "type": "string", "nullable": true }, + "status": { + "type": "string", + "enum": ["draft", "active", "suspended", "draining", "retired"] + }, + "secretVersion": { + "type": "integer", + "minimum": 0, + "exclusiveMinimum": 0 + }, + "lastTestedAt": { "type": "string", "nullable": true }, + "lastTestStatus": { + "type": "string", + "enum": ["success", "failed"], + "nullable": true + }, + "lastTestError": { "type": "string", "nullable": true }, + "activatedAt": { "type": "string", "nullable": true }, + "drainUntil": { "type": "string", "nullable": true }, + "retiredAt": { "type": "string", "nullable": true }, + "updatedAt": { "type": "string" } + }, + "required": [ + "espId", + "name", + "provider", + "host", + "port", + "secure", + "hasPassword", + "status", + "secretVersion" + ] + } + } + } + }, + "403": { + "description": "403", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + }, + "404": { + "description": "404", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + }, + "409": { + "description": "409", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + } + } + } + }, + "/settings/esps/{espId}/retire": { + "post": { + "summary": "Drain or cancel a team ESP", + "tags": ["Settings"], + "parameters": [ + { + "name": "espId", + "in": "path", + "required": true, + "schema": { "type": "string" } + } + ], + "requestBody": { + "description": "Body", + "content": { + "application/json": { + "schema": { + "discriminator": { "propertyName": "transition" }, + "oneOf": [ + { + "type": "object", + "properties": { + "transition": { "type": "string", "enum": ["drain"] }, + "drainUntil": { "type": "string", "format": "date-time" } + }, + "required": ["transition"] + }, + { + "type": "object", + "properties": { + "transition": { "type": "string", "enum": ["cancel"] } + }, + "required": ["transition"] + } + ] + } + } + } + }, + "responses": { + "200": { + "description": "200", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "espId": { "type": "string" }, + "name": { "type": "string" }, + "provider": { "type": "string" }, + "host": { "type": "string" }, + "port": { "type": "number" }, + "secure": { "type": "boolean" }, + "username": { "type": "string", "nullable": true }, + "hasPassword": { "type": "boolean" }, + "fromName": { "type": "string", "nullable": true }, + "fromEmail": { "type": "string", "nullable": true }, + "status": { + "type": "string", + "enum": ["draft", "active", "suspended", "draining", "retired"] + }, + "secretVersion": { + "type": "integer", + "minimum": 0, + "exclusiveMinimum": 0 + }, + "lastTestedAt": { "type": "string", "nullable": true }, + "lastTestStatus": { + "type": "string", + "enum": ["success", "failed"], + "nullable": true + }, + "lastTestError": { "type": "string", "nullable": true }, + "activatedAt": { "type": "string", "nullable": true }, + "drainUntil": { "type": "string", "nullable": true }, + "retiredAt": { "type": "string", "nullable": true }, + "updatedAt": { "type": "string" } + }, + "required": [ + "espId", + "name", + "provider", + "host", + "port", + "secure", + "hasPassword", + "status", + "secretVersion" + ] + } + } + } + }, + "403": { + "description": "403", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + }, + "404": { + "description": "404", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + }, + "409": { + "description": "409", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + } + } + } + }, + "/settings/general": { + "get": { + "description": "Returns defaults (all-null fields) when nothing has been saved yet.", + "summary": "Get the team's general settings", + "tags": ["Settings"], + "parameters": [], + "responses": { + "200": { + "description": "200", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "mailingAddress": { "type": "string", "nullable": true }, + "updatedAt": { "type": "string", "nullable": true } + }, + "required": ["mailingAddress"] + } + } + } + } + } + }, + "put": { + "description": "Omitted fields are left unchanged; send an empty string to clear a field.", + "summary": "Update the team's general settings", + "tags": ["Settings"], + "parameters": [], + "requestBody": { + "description": "Body", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "mailingAddress": { "type": "string" } } + } + } + } + }, + "responses": { + "200": { + "description": "200", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "mailingAddress": { "type": "string", "nullable": true }, + "updatedAt": { "type": "string", "nullable": true } + }, + "required": ["mailingAddress"] + } + } + } + } + } + } + }, + "/teams": { + "get": { + "summary": "List the teams the current user belongs to", + "tags": ["Teams"], + "parameters": [], + "responses": { + "200": { + "description": "200", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "type": "object", + "properties": { + "teamId": { "type": "string" }, + "organizationId": { "type": "string" }, + "organizationName": { "type": "string" }, + "name": { "type": "string" }, + "status": { + "type": "string", + "enum": ["active", "sending_suspended", "archived"] + }, + "externalId": { "type": "string", "nullable": true }, + "createdAt": { "type": "string", "nullable": true }, + "updatedAt": { "type": "string", "nullable": true }, + "viewerIsMember": { "type": "boolean" } + }, + "required": ["teamId", "name", "status"] + } + } + }, + "required": ["items"] + } + } + } + } + } + }, + "post": { + "summary": "Create a new team", + "tags": ["Teams"], + "parameters": [], + "requestBody": { + "description": "Body", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "name": { "type": "string", "minLength": 1 } }, + "required": ["name"] + } + } + } + }, + "responses": { + "201": { + "description": "201", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "teamId": { "type": "string" }, + "organizationId": { "type": "string" }, + "organizationName": { "type": "string" }, + "name": { "type": "string" }, + "status": { + "type": "string", + "enum": ["active", "sending_suspended", "archived"] + }, + "externalId": { "type": "string", "nullable": true }, + "createdAt": { "type": "string", "nullable": true }, + "updatedAt": { "type": "string", "nullable": true }, + "viewerIsMember": { "type": "boolean" } + }, + "required": ["teamId", "name", "status"] + } + } + } + }, + "402": { + "description": "402", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + }, + "403": { + "description": "403", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + }, + "409": { + "description": "409", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + } + } + } + }, + "/teams/{teamId}": { + "patch": { + "summary": "Rename a team", + "tags": ["Teams"], + "parameters": [ + { + "name": "teamId", + "in": "path", + "required": true, + "schema": { "type": "string" } + } + ], + "requestBody": { + "description": "Body", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "name": { "type": "string", "minLength": 1 } }, + "required": ["name"] + } + } + } + }, + "responses": { + "200": { + "description": "200", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "teamId": { "type": "string" }, + "organizationId": { "type": "string" }, + "organizationName": { "type": "string" }, + "name": { "type": "string" }, + "status": { + "type": "string", + "enum": ["active", "sending_suspended", "archived"] + }, + "externalId": { "type": "string", "nullable": true }, + "createdAt": { "type": "string", "nullable": true }, + "updatedAt": { "type": "string", "nullable": true }, + "viewerIsMember": { "type": "boolean" } + }, + "required": ["teamId", "name", "status"] + } + } + } + }, + "404": { + "description": "404", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + } + } + }, + "delete": { + "summary": "Archive a team", + "tags": ["Teams"], + "parameters": [ + { + "name": "teamId", + "in": "path", + "required": true, + "schema": { "type": "string" } + } + ], + "responses": { + "204": { "description": "204" }, + "403": { + "description": "403", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + }, + "404": { + "description": "404", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + } + } + } + }, + "/teams/{teamId}/keys": { + "get": { + "summary": "List a team's API keys", + "tags": ["Teams"], + "parameters": [ + { + "name": "teamId", + "in": "path", + "required": true, + "schema": { "type": "string" } + } + ], + "responses": { + "200": { + "description": "200", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "type": "object", + "properties": { + "keyId": { "type": "string" }, + "keyPrefix": { "type": "string" }, + "name": { "type": "string" }, + "expiresAt": { "type": "string", "nullable": true }, + "lastUsedAt": { "type": "string", "nullable": true }, + "revokedAt": { "type": "string", "nullable": true }, + "createdAt": { "type": "string", "nullable": true } + }, + "required": ["keyId", "keyPrefix", "name"] + } + } + }, + "required": ["items"] + } + } + } + }, + "404": { + "description": "404", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + } + } + }, + "post": { + "description": "The response's `key` field is the full secret and is only ever returned once — store it securely. Subsequent listings only expose the key's prefix.", + "summary": "Create a new API key for a team", + "tags": ["Teams"], + "parameters": [ + { + "name": "teamId", + "in": "path", + "required": true, + "schema": { "type": "string" } + } + ], + "requestBody": { + "description": "Body", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "name": { "type": "string", "minLength": 1 } }, + "required": ["name"] + } + } + } + }, + "responses": { + "201": { + "description": "201", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "keyId": { "type": "string" }, + "keyPrefix": { "type": "string" }, + "name": { "type": "string" }, + "expiresAt": { "type": "string", "nullable": true }, + "lastUsedAt": { "type": "string", "nullable": true }, + "revokedAt": { "type": "string", "nullable": true }, + "createdAt": { "type": "string", "nullable": true }, + "key": { "type": "string" } + }, + "required": ["keyId", "keyPrefix", "name", "key"] + } + } + } + }, + "404": { + "description": "404", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + } + } + } + }, + "/teams/{teamId}/keys/{keyId}": { + "delete": { + "summary": "Revoke an API key by its id", + "tags": ["Teams"], + "parameters": [ + { + "name": "teamId", + "in": "path", + "required": true, + "schema": { "type": "string" } + }, + { + "name": "keyId", + "in": "path", + "required": true, + "schema": { "type": "string" } + } + ], + "responses": { + "204": { "description": "204" }, + "404": { + "description": "404", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + } + } + } + }, + "/organizations": { + "get": { + "summary": "List organizations for the current user", + "tags": ["Organizations"], + "parameters": [], + "responses": { + "200": { + "description": "200", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "type": "object", + "properties": { + "organizationId": { "type": "string" }, + "name": { "type": "string" }, + "status": { + "type": "string", + "enum": [ + "pending_payment", + "active", + "suspended", + "abandoned", + "closed" + ] + }, + "createdAt": { "type": "string" }, + "updatedAt": { "type": "string" } + }, + "required": [ + "organizationId", + "name", + "status", + "createdAt", + "updatedAt" + ] + } + }, + "ownsFreeOrganization": { "type": "boolean" } + }, + "required": ["items", "ownsFreeOrganization"] + } + } + } + }, + "403": { + "description": "403", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + } + } + }, + "post": { + "summary": "Create an organization", + "tags": ["Organizations"], + "parameters": [], + "requestBody": { + "description": "Body", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "name": { "type": "string", "minLength": 1 } }, + "required": ["name"] + } + } + } + }, + "responses": { + "201": { + "description": "201", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "organizationId": { "type": "string" }, + "name": { "type": "string" }, + "status": { + "type": "string", + "enum": [ + "pending_payment", + "active", + "suspended", + "abandoned", + "closed" + ] + }, + "createdAt": { "type": "string" }, + "updatedAt": { "type": "string" } + }, + "required": [ + "organizationId", + "name", + "status", + "createdAt", + "updatedAt" + ] + } + } + } + }, + "403": { + "description": "403", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + }, + "409": { + "description": "409", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + } + } + } + }, + "/organizations/{organizationId}": { + "get": { + "summary": "Get an organization", + "tags": ["Organizations"], + "parameters": [ + { + "name": "organizationId", + "in": "path", + "required": true, + "schema": { "type": "string" } + } + ], + "responses": { + "200": { + "description": "200", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "organizationId": { "type": "string" }, + "name": { "type": "string" }, + "status": { + "type": "string", + "enum": [ + "pending_payment", + "active", + "suspended", + "abandoned", + "closed" + ] + }, + "createdAt": { "type": "string" }, + "updatedAt": { "type": "string" } + }, + "required": [ + "organizationId", + "name", + "status", + "createdAt", + "updatedAt" + ] + } + } + } + }, + "403": { + "description": "403", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + }, + "404": { + "description": "404", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + } + } + }, + "patch": { + "summary": "Update an organization", + "tags": ["Organizations"], + "parameters": [ + { + "name": "organizationId", + "in": "path", + "required": true, + "schema": { "type": "string" } + } + ], + "requestBody": { + "description": "Body", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "name": { "type": "string", "minLength": 1 } }, + "required": ["name"] + } + } + } + }, + "responses": { + "200": { + "description": "200", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "organizationId": { "type": "string" }, + "name": { "type": "string" }, + "status": { + "type": "string", + "enum": [ + "pending_payment", + "active", + "suspended", + "abandoned", + "closed" + ] + }, + "createdAt": { "type": "string" }, + "updatedAt": { "type": "string" } + }, + "required": [ + "organizationId", + "name", + "status", + "createdAt", + "updatedAt" + ] + } + } + } + }, + "403": { + "description": "403", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + }, + "404": { + "description": "404", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + } + } + }, + "delete": { + "summary": "Close an organization", + "tags": ["Organizations"], + "parameters": [ + { + "name": "organizationId", + "in": "path", + "required": true, + "schema": { "type": "string" } + } + ], + "responses": { + "204": { "description": "204" }, + "401": { + "description": "401", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + }, + "403": { + "description": "403", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + }, + "404": { + "description": "404", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + }, + "409": { + "description": "409", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + }, + "503": { + "description": "503", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + } + } + } + }, + "/organizations/{organizationId}/abandon": { + "post": { + "summary": "Hide a pending-payment organization after a cancelled checkout", + "tags": ["Organizations"], + "parameters": [ + { + "name": "organizationId", + "in": "path", + "required": true, + "schema": { "type": "string" } + } + ], + "responses": { + "204": { "description": "204" }, + "401": { + "description": "401", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + }, + "403": { + "description": "403", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + }, + "404": { + "description": "404", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + }, + "409": { + "description": "409", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + } + } + } + }, + "/organizations/{organizationId}/members": { + "get": { + "summary": "List organization members", + "tags": ["Organizations"], + "parameters": [ + { + "name": "organizationId", + "in": "path", + "required": true, + "schema": { "type": "string" } + } + ], + "responses": { + "200": { + "description": "200", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "type": "object", + "properties": { + "userId": { "type": "string" }, + "email": { "type": "string", "format": "email" }, + "name": { "type": "string" }, + "image": { "type": "string", "nullable": true }, + "role": { + "type": "string", + "enum": ["owner", "admin", "member"] + }, + "createdAt": { "type": "string" }, + "updatedAt": { "type": "string" } + }, + "required": [ + "userId", + "email", + "name", + "image", + "role", + "createdAt", + "updatedAt" + ] + } + } + }, + "required": ["items"] + } + } + } + }, + "403": { + "description": "403", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + }, + "404": { + "description": "404", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + } + } + }, + "post": { + "summary": "Add an existing user to an organization", + "tags": ["Organizations"], + "parameters": [ + { + "name": "organizationId", + "in": "path", + "required": true, + "schema": { "type": "string" } + } + ], + "requestBody": { + "description": "Body", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "email": { "type": "string", "format": "email" }, + "role": { "type": "string", "enum": ["owner", "admin", "member"] } + }, + "required": ["email", "role"] + } + } + } + }, + "responses": { + "201": { + "description": "201", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "userId": { "type": "string" }, + "email": { "type": "string", "format": "email" }, + "name": { "type": "string" }, + "image": { "type": "string", "nullable": true }, + "role": { "type": "string", "enum": ["owner", "admin", "member"] }, + "createdAt": { "type": "string" }, + "updatedAt": { "type": "string" } + }, + "required": [ + "userId", + "email", + "name", + "image", + "role", + "createdAt", + "updatedAt" + ] + } + } + } + }, + "403": { + "description": "403", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + }, + "404": { + "description": "404", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + }, + "409": { + "description": "409", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + } + } + } + }, + "/organizations/{organizationId}/members/{userId}": { + "patch": { + "summary": "Change an organization member role", + "tags": ["Organizations"], + "parameters": [ + { + "name": "organizationId", + "in": "path", + "required": true, + "schema": { "type": "string" } + }, + { + "name": "userId", + "in": "path", + "required": true, + "schema": { "type": "string" } + } + ], + "requestBody": { + "description": "Body", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "role": { "type": "string", "enum": ["owner", "admin", "member"] } + }, + "required": ["role"] + } + } + } + }, + "responses": { + "200": { + "description": "200", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "userId": { "type": "string" }, + "email": { "type": "string", "format": "email" }, + "name": { "type": "string" }, + "image": { "type": "string", "nullable": true }, + "role": { "type": "string", "enum": ["owner", "admin", "member"] }, + "createdAt": { "type": "string" }, + "updatedAt": { "type": "string" } + }, + "required": [ + "userId", + "email", + "name", + "image", + "role", + "createdAt", + "updatedAt" + ] + } + } + } + }, + "403": { + "description": "403", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + }, + "404": { + "description": "404", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + }, + "409": { + "description": "409", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + } + } + }, + "delete": { + "summary": "Remove an organization member", + "tags": ["Organizations"], + "parameters": [ + { + "name": "organizationId", + "in": "path", + "required": true, + "schema": { "type": "string" } + }, + { + "name": "userId", + "in": "path", + "required": true, + "schema": { "type": "string" } + } + ], + "responses": { + "204": { "description": "204" }, + "403": { + "description": "403", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + }, + "404": { + "description": "404", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + }, + "409": { + "description": "409", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + } + } + } + }, + "/organizations/{organizationId}/teams": { + "get": { + "summary": "List teams in an organization", + "tags": ["Organizations"], + "parameters": [ + { + "name": "organizationId", + "in": "path", + "required": true, + "schema": { "type": "string" } + } + ], + "responses": { + "200": { + "description": "200", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "type": "object", + "properties": { + "teamId": { "type": "string" }, + "organizationId": { "type": "string" }, + "organizationName": { "type": "string" }, + "name": { "type": "string" }, + "status": { + "type": "string", + "enum": ["active", "sending_suspended", "archived"] + }, + "externalId": { "type": "string", "nullable": true }, + "createdAt": { "type": "string", "nullable": true }, + "updatedAt": { "type": "string", "nullable": true }, + "viewerIsMember": { "type": "boolean" } + }, + "required": ["teamId", "name", "status"] + } + } + }, + "required": ["items"] + } + } + } + }, + "403": { + "description": "403", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + }, + "404": { + "description": "404", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + } + } + }, + "post": { + "summary": "Create a human-managed team in an organization", + "tags": ["Organizations"], + "parameters": [ + { + "name": "organizationId", + "in": "path", + "required": true, + "schema": { "type": "string" } + } + ], + "requestBody": { + "description": "Body", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "name": { "type": "string", "minLength": 1 } }, + "required": ["name"] + } + } + } + }, + "responses": { + "201": { + "description": "201", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "teamId": { "type": "string" }, + "organizationId": { "type": "string" }, + "organizationName": { "type": "string" }, + "name": { "type": "string" }, + "status": { + "type": "string", + "enum": ["active", "sending_suspended", "archived"] + }, + "externalId": { "type": "string", "nullable": true }, + "createdAt": { "type": "string", "nullable": true }, + "updatedAt": { "type": "string", "nullable": true }, + "viewerIsMember": { "type": "boolean" } + }, + "required": ["teamId", "name", "status"] + } + } + } + }, + "403": { + "description": "403", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + }, + "404": { + "description": "404", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + }, + "409": { + "description": "409", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + } + } + } + }, + "/organizations/{organizationId}/teams/{teamId}": { + "get": { + "summary": "Get organization team metadata", + "tags": ["Organizations"], + "parameters": [ + { + "name": "organizationId", + "in": "path", + "required": true, + "schema": { "type": "string" } + }, + { + "name": "teamId", + "in": "path", + "required": true, + "schema": { "type": "string" } + } + ], + "responses": { + "200": { + "description": "200", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "teamId": { "type": "string" }, + "organizationId": { "type": "string" }, + "organizationName": { "type": "string" }, + "name": { "type": "string" }, + "status": { + "type": "string", + "enum": ["active", "sending_suspended", "archived"] + }, + "externalId": { "type": "string", "nullable": true }, + "createdAt": { "type": "string", "nullable": true }, + "updatedAt": { "type": "string", "nullable": true }, + "viewerIsMember": { "type": "boolean" } + }, + "required": ["teamId", "name", "status"] + } + } + } + }, + "403": { + "description": "403", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + }, + "404": { + "description": "404", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + } + } + }, + "patch": { + "summary": "Rename an organization team", + "tags": ["Organizations"], + "parameters": [ + { + "name": "organizationId", + "in": "path", + "required": true, + "schema": { "type": "string" } + }, + { + "name": "teamId", + "in": "path", + "required": true, + "schema": { "type": "string" } + } + ], + "requestBody": { + "description": "Body", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "name": { "type": "string", "minLength": 1 } }, + "required": ["name"] + } + } + } + }, + "responses": { + "200": { + "description": "200", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "teamId": { "type": "string" }, + "organizationId": { "type": "string" }, + "organizationName": { "type": "string" }, + "name": { "type": "string" }, + "status": { + "type": "string", + "enum": ["active", "sending_suspended", "archived"] + }, + "externalId": { "type": "string", "nullable": true }, + "createdAt": { "type": "string", "nullable": true }, + "updatedAt": { "type": "string", "nullable": true }, + "viewerIsMember": { "type": "boolean" } + }, + "required": ["teamId", "name", "status"] + } + } + } + }, + "403": { + "description": "403", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + }, + "404": { + "description": "404", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + } + } + }, + "delete": { + "summary": "Archive an organization team", + "tags": ["Organizations"], + "parameters": [ + { + "name": "organizationId", + "in": "path", + "required": true, + "schema": { "type": "string" } + }, + { + "name": "teamId", + "in": "path", + "required": true, + "schema": { "type": "string" } + } + ], + "responses": { + "204": { "description": "204" }, + "403": { + "description": "403", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + }, + "404": { + "description": "404", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + } + } + } + }, + "/organizations/{organizationId}/keys": { + "get": { + "summary": "List organization API keys", + "tags": ["Organizations"], + "parameters": [ + { + "name": "organizationId", + "in": "path", + "required": true, + "schema": { "type": "string" } + } + ], + "responses": { + "200": { + "description": "200", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "type": "object", + "properties": { + "keyId": { "type": "string" }, + "name": { "type": "string" }, + "keyPrefix": { "type": "string" }, + "scopes": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "organization:read", + "teams:provision", + "teams:read", + "teams:manage", + "teams:keys", + "esps:read", + "esps:manage", + "grants:manage", + "usage:read" + ] + } + }, + "expiresAt": { "type": "string", "nullable": true }, + "lastUsedAt": { "type": "string", "nullable": true }, + "revokedAt": { "type": "string", "nullable": true }, + "createdAt": { "type": "string" } + }, + "required": [ + "keyId", + "name", + "keyPrefix", + "scopes", + "expiresAt", + "lastUsedAt", + "revokedAt", + "createdAt" + ] + } + } + }, + "required": ["items"] + } + } + } + }, + "403": { + "description": "403", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + }, + "404": { + "description": "404", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + } + } + }, + "post": { + "summary": "Create an organization API key", + "tags": ["Organizations"], + "parameters": [ + { + "name": "organizationId", + "in": "path", + "required": true, + "schema": { "type": "string" } + } + ], + "requestBody": { + "description": "Body", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "name": { "type": "string", "minLength": 1 }, + "scopes": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "organization:read", + "teams:provision", + "teams:read", + "teams:manage", + "teams:keys", + "esps:read", + "esps:manage", + "grants:manage", + "usage:read" + ] + }, + "minItems": 1 + }, + "expiresAt": { + "type": "string", + "format": "date-time", + "nullable": true + } + }, + "required": ["name", "scopes"] + } + } + } + }, + "responses": { + "201": { + "description": "201", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "keyId": { "type": "string" }, + "name": { "type": "string" }, + "keyPrefix": { "type": "string" }, + "scopes": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "organization:read", + "teams:provision", + "teams:read", + "teams:manage", + "teams:keys", + "esps:read", + "esps:manage", + "grants:manage", + "usage:read" + ] + } + }, + "expiresAt": { "type": "string", "nullable": true }, + "lastUsedAt": { "type": "string", "nullable": true }, + "revokedAt": { "type": "string", "nullable": true }, + "createdAt": { "type": "string" }, + "key": { "type": "string" } + }, + "required": [ + "keyId", + "name", + "keyPrefix", + "scopes", + "expiresAt", + "lastUsedAt", + "revokedAt", + "createdAt", + "key" + ] + } + } + } + }, + "403": { + "description": "403", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + }, + "404": { + "description": "404", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + } + } + } + }, + "/organizations/{organizationId}/keys/{keyId}": { + "delete": { + "summary": "Revoke an organization API key", + "tags": ["Organizations"], + "parameters": [ + { + "name": "organizationId", + "in": "path", + "required": true, + "schema": { "type": "string" } + }, + { + "name": "keyId", + "in": "path", + "required": true, + "schema": { "type": "string" } + } + ], + "responses": { + "204": { "description": "204" }, + "403": { + "description": "403", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + }, + "404": { + "description": "404", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + } + } + } + }, + "/organizations/{organizationId}/esps": { + "get": { + "summary": "List organization-owned ESP configurations", + "tags": ["Organizations"], + "parameters": [ + { + "name": "organizationId", + "in": "path", + "required": true, + "schema": { "type": "string" } + } + ], + "responses": { + "200": { + "description": "200", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "type": "object", + "properties": { + "espId": { "type": "string" }, + "name": { "type": "string" }, + "provider": { "type": "string" }, + "host": { "type": "string" }, + "port": { "type": "number" }, + "secure": { "type": "boolean" }, + "username": { "type": "string", "nullable": true }, + "hasPassword": { "type": "boolean" }, + "fromName": { "type": "string", "nullable": true }, + "fromEmail": { "type": "string", "nullable": true }, + "status": { + "type": "string", + "enum": [ + "draft", + "active", + "suspended", + "draining", + "retired" + ] + }, + "secretVersion": { + "type": "integer", + "minimum": 0, + "exclusiveMinimum": 0 + }, + "lastTestedAt": { "type": "string", "nullable": true }, + "lastTestStatus": { + "type": "string", + "enum": ["success", "failed"], + "nullable": true + }, + "lastTestError": { "type": "string", "nullable": true }, + "activatedAt": { "type": "string", "nullable": true }, + "drainUntil": { "type": "string", "nullable": true }, + "retiredAt": { "type": "string", "nullable": true }, + "updatedAt": { "type": "string" } + }, + "required": [ + "espId", + "name", + "provider", + "host", + "port", + "secure", + "hasPassword", + "status", + "secretVersion" + ] + } + } + }, + "required": ["items"] + } + } + } + }, + "403": { + "description": "403", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + }, + "404": { + "description": "404", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + } + } + }, + "post": { + "summary": "Create an organization-owned ESP configuration", + "tags": ["Organizations"], + "parameters": [ + { + "name": "organizationId", + "in": "path", + "required": true, + "schema": { "type": "string" } + } + ], + "requestBody": { + "description": "Body", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "provider": { + "type": "string", + "enum": ["smtp", "sendgrid", "mailgun", "postmark", "ses", "resend"] + }, + "host": { "type": "string", "minLength": 1 }, + "port": { "type": "integer", "minimum": 1, "maximum": 65535 }, + "secure": { "type": "boolean" }, + "username": { "type": "string" }, + "password": { "type": "string" }, + "fromName": { "type": "string" }, + "fromEmail": { "type": "string", "format": "email" }, + "name": { "type": "string", "minLength": 1, "maxLength": 100 } + }, + "required": ["provider", "host", "port", "secure", "name"] + } + } + } + }, + "responses": { + "201": { + "description": "201", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "espId": { "type": "string" }, + "name": { "type": "string" }, + "provider": { "type": "string" }, + "host": { "type": "string" }, + "port": { "type": "number" }, + "secure": { "type": "boolean" }, + "username": { "type": "string", "nullable": true }, + "hasPassword": { "type": "boolean" }, + "fromName": { "type": "string", "nullable": true }, + "fromEmail": { "type": "string", "nullable": true }, + "status": { + "type": "string", + "enum": ["draft", "active", "suspended", "draining", "retired"] + }, + "secretVersion": { + "type": "integer", + "minimum": 0, + "exclusiveMinimum": 0 + }, + "lastTestedAt": { "type": "string", "nullable": true }, + "lastTestStatus": { + "type": "string", + "enum": ["success", "failed"], + "nullable": true + }, + "lastTestError": { "type": "string", "nullable": true }, + "activatedAt": { "type": "string", "nullable": true }, + "drainUntil": { "type": "string", "nullable": true }, + "retiredAt": { "type": "string", "nullable": true }, + "updatedAt": { "type": "string" } + }, + "required": [ + "espId", + "name", + "provider", + "host", + "port", + "secure", + "hasPassword", + "status", + "secretVersion" + ] + } + } + } + }, + "403": { + "description": "403", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + }, + "404": { + "description": "404", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + } + } + } + }, + "/organizations/{organizationId}/esps/{espId}": { + "get": { + "summary": "Get an organization-owned ESP configuration", + "tags": ["Organizations"], + "parameters": [ + { + "name": "organizationId", + "in": "path", + "required": true, + "schema": { "type": "string" } + }, + { + "name": "espId", + "in": "path", + "required": true, + "schema": { "type": "string" } + } + ], + "responses": { + "200": { + "description": "200", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "espId": { "type": "string" }, + "name": { "type": "string" }, + "provider": { "type": "string" }, + "host": { "type": "string" }, + "port": { "type": "number" }, + "secure": { "type": "boolean" }, + "username": { "type": "string", "nullable": true }, + "hasPassword": { "type": "boolean" }, + "fromName": { "type": "string", "nullable": true }, + "fromEmail": { "type": "string", "nullable": true }, + "status": { + "type": "string", + "enum": ["draft", "active", "suspended", "draining", "retired"] + }, + "secretVersion": { + "type": "integer", + "minimum": 0, + "exclusiveMinimum": 0 + }, + "lastTestedAt": { "type": "string", "nullable": true }, + "lastTestStatus": { + "type": "string", + "enum": ["success", "failed"], + "nullable": true + }, + "lastTestError": { "type": "string", "nullable": true }, + "activatedAt": { "type": "string", "nullable": true }, + "drainUntil": { "type": "string", "nullable": true }, + "retiredAt": { "type": "string", "nullable": true }, + "updatedAt": { "type": "string" } + }, + "required": [ + "espId", + "name", + "provider", + "host", + "port", + "secure", + "hasPassword", + "status", + "secretVersion" + ] + } + } + } + }, + "403": { + "description": "403", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + }, + "404": { + "description": "404", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + } + } + }, + "patch": { + "summary": "Update an organization-owned ESP configuration", + "tags": ["Organizations"], + "parameters": [ + { + "name": "organizationId", + "in": "path", + "required": true, + "schema": { "type": "string" } + }, + { + "name": "espId", + "in": "path", + "required": true, + "schema": { "type": "string" } + } + ], + "requestBody": { + "description": "Body", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "name": { "type": "string", "minLength": 1, "maxLength": 100 }, + "provider": { + "type": "string", + "enum": ["smtp", "sendgrid", "mailgun", "postmark", "ses", "resend"] + }, + "host": { "type": "string", "minLength": 1 }, + "port": { "type": "integer", "minimum": 1, "maximum": 65535 }, + "secure": { "type": "boolean" }, + "username": { "type": "string" }, + "password": { "type": "string" }, + "fromName": { "type": "string" }, + "fromEmail": { + "oneOf": [ + { "type": "string", "format": "email" }, + { "type": "string", "enum": [""] } + ] + } + } + } + } + } + }, + "responses": { + "200": { + "description": "200", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "espId": { "type": "string" }, + "name": { "type": "string" }, + "provider": { "type": "string" }, + "host": { "type": "string" }, + "port": { "type": "number" }, + "secure": { "type": "boolean" }, + "username": { "type": "string", "nullable": true }, + "hasPassword": { "type": "boolean" }, + "fromName": { "type": "string", "nullable": true }, + "fromEmail": { "type": "string", "nullable": true }, + "status": { + "type": "string", + "enum": ["draft", "active", "suspended", "draining", "retired"] + }, + "secretVersion": { + "type": "integer", + "minimum": 0, + "exclusiveMinimum": 0 + }, + "lastTestedAt": { "type": "string", "nullable": true }, + "lastTestStatus": { + "type": "string", + "enum": ["success", "failed"], + "nullable": true + }, + "lastTestError": { "type": "string", "nullable": true }, + "activatedAt": { "type": "string", "nullable": true }, + "drainUntil": { "type": "string", "nullable": true }, + "retiredAt": { "type": "string", "nullable": true }, + "updatedAt": { "type": "string" } + }, + "required": [ + "espId", + "name", + "provider", + "host", + "port", + "secure", + "hasPassword", + "status", + "secretVersion" + ] + } + } + } + }, + "403": { + "description": "403", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + }, + "404": { + "description": "404", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + }, + "409": { + "description": "409", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + } + } + }, + "delete": { + "summary": "Delete an eligible never-activated organization ESP", + "tags": ["Organizations"], + "parameters": [ + { + "name": "organizationId", + "in": "path", + "required": true, + "schema": { "type": "string" } + }, + { + "name": "espId", + "in": "path", + "required": true, + "schema": { "type": "string" } + } + ], + "responses": { + "204": { "description": "204" }, + "403": { + "description": "403", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + }, + "404": { + "description": "404", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + }, + "409": { + "description": "409", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + } + } + } + }, + "/organizations/{organizationId}/esps/{espId}/test": { + "post": { + "summary": "Test an organization-owned ESP configuration", + "tags": ["Organizations"], + "parameters": [ + { + "name": "organizationId", + "in": "path", + "required": true, + "schema": { "type": "string" } + }, + { + "name": "espId", + "in": "path", + "required": true, + "schema": { "type": "string" } + } + ], + "requestBody": { + "description": "Body", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "to": { "type": "string", "format": "email" } } + } + } + } + }, + "responses": { + "200": { + "description": "200", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { "type": "boolean" }, + "error": { "type": "string" } + }, + "required": ["success"] + } + } + } + }, + "400": { + "description": "400", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + }, + "403": { + "description": "403", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + }, + "404": { + "description": "404", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + }, + "422": { + "description": "422", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + }, + "502": { + "description": "502", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { "type": "boolean" }, + "error": { "type": "string" } + }, + "required": ["success"] + } + } + } + } + } + } + }, + "/organizations/{organizationId}/esps/{espId}/activate": { + "post": { + "summary": "Activate a verified organization ESP", + "tags": ["Organizations"], + "parameters": [ + { + "name": "organizationId", + "in": "path", + "required": true, + "schema": { "type": "string" } + }, + { + "name": "espId", + "in": "path", + "required": true, + "schema": { "type": "string" } + } + ], + "responses": { + "200": { + "description": "200", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "espId": { "type": "string" }, + "name": { "type": "string" }, + "provider": { "type": "string" }, + "host": { "type": "string" }, + "port": { "type": "number" }, + "secure": { "type": "boolean" }, + "username": { "type": "string", "nullable": true }, + "hasPassword": { "type": "boolean" }, + "fromName": { "type": "string", "nullable": true }, + "fromEmail": { "type": "string", "nullable": true }, + "status": { + "type": "string", + "enum": ["draft", "active", "suspended", "draining", "retired"] + }, + "secretVersion": { + "type": "integer", + "minimum": 0, + "exclusiveMinimum": 0 + }, + "lastTestedAt": { "type": "string", "nullable": true }, + "lastTestStatus": { + "type": "string", + "enum": ["success", "failed"], + "nullable": true + }, + "lastTestError": { "type": "string", "nullable": true }, + "activatedAt": { "type": "string", "nullable": true }, + "drainUntil": { "type": "string", "nullable": true }, + "retiredAt": { "type": "string", "nullable": true }, + "updatedAt": { "type": "string" } + }, + "required": [ + "espId", + "name", + "provider", + "host", + "port", + "secure", + "hasPassword", + "status", + "secretVersion" + ] + } + } + } + }, + "403": { + "description": "403", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + }, + "404": { + "description": "404", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + }, + "409": { + "description": "409", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + }, + "422": { + "description": "422", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + } + } + } + }, + "/organizations/{organizationId}/esps/{espId}/suspend": { + "post": { + "summary": "Suspend an organization ESP", + "tags": ["Organizations"], + "parameters": [ + { + "name": "organizationId", + "in": "path", + "required": true, + "schema": { "type": "string" } + }, + { + "name": "espId", + "in": "path", + "required": true, + "schema": { "type": "string" } + } + ], + "responses": { + "200": { + "description": "200", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "espId": { "type": "string" }, + "name": { "type": "string" }, + "provider": { "type": "string" }, + "host": { "type": "string" }, + "port": { "type": "number" }, + "secure": { "type": "boolean" }, + "username": { "type": "string", "nullable": true }, + "hasPassword": { "type": "boolean" }, + "fromName": { "type": "string", "nullable": true }, + "fromEmail": { "type": "string", "nullable": true }, + "status": { + "type": "string", + "enum": ["draft", "active", "suspended", "draining", "retired"] + }, + "secretVersion": { + "type": "integer", + "minimum": 0, + "exclusiveMinimum": 0 + }, + "lastTestedAt": { "type": "string", "nullable": true }, + "lastTestStatus": { + "type": "string", + "enum": ["success", "failed"], + "nullable": true + }, + "lastTestError": { "type": "string", "nullable": true }, + "activatedAt": { "type": "string", "nullable": true }, + "drainUntil": { "type": "string", "nullable": true }, + "retiredAt": { "type": "string", "nullable": true }, + "updatedAt": { "type": "string" } + }, + "required": [ + "espId", + "name", + "provider", + "host", + "port", + "secure", + "hasPassword", + "status", + "secretVersion" + ] + } + } + } + }, + "403": { + "description": "403", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + }, + "404": { + "description": "404", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + }, + "409": { + "description": "409", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + } + } + } + }, + "/organizations/{organizationId}/esps/{espId}/resume": { + "post": { + "summary": "Resume an organization ESP", + "tags": ["Organizations"], + "parameters": [ + { + "name": "organizationId", + "in": "path", + "required": true, + "schema": { "type": "string" } + }, + { + "name": "espId", + "in": "path", + "required": true, + "schema": { "type": "string" } + } + ], + "responses": { + "200": { + "description": "200", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "espId": { "type": "string" }, + "name": { "type": "string" }, + "provider": { "type": "string" }, + "host": { "type": "string" }, + "port": { "type": "number" }, + "secure": { "type": "boolean" }, + "username": { "type": "string", "nullable": true }, + "hasPassword": { "type": "boolean" }, + "fromName": { "type": "string", "nullable": true }, + "fromEmail": { "type": "string", "nullable": true }, + "status": { + "type": "string", + "enum": ["draft", "active", "suspended", "draining", "retired"] + }, + "secretVersion": { + "type": "integer", + "minimum": 0, + "exclusiveMinimum": 0 + }, + "lastTestedAt": { "type": "string", "nullable": true }, + "lastTestStatus": { + "type": "string", + "enum": ["success", "failed"], + "nullable": true + }, + "lastTestError": { "type": "string", "nullable": true }, + "activatedAt": { "type": "string", "nullable": true }, + "drainUntil": { "type": "string", "nullable": true }, + "retiredAt": { "type": "string", "nullable": true }, + "updatedAt": { "type": "string" } + }, + "required": [ + "espId", + "name", + "provider", + "host", + "port", + "secure", + "hasPassword", + "status", + "secretVersion" + ] + } + } + } + }, + "403": { + "description": "403", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + }, + "404": { + "description": "404", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + }, + "409": { + "description": "409", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + } + } + } + }, + "/organizations/{organizationId}/esps/{espId}/retire": { + "post": { + "summary": "Drain or cancel an organization ESP", + "tags": ["Organizations"], + "parameters": [ + { + "name": "organizationId", + "in": "path", + "required": true, + "schema": { "type": "string" } + }, + { + "name": "espId", + "in": "path", + "required": true, + "schema": { "type": "string" } + } + ], + "requestBody": { + "description": "Body", + "content": { + "application/json": { + "schema": { + "discriminator": { "propertyName": "transition" }, + "oneOf": [ + { + "type": "object", + "properties": { + "transition": { "type": "string", "enum": ["drain"] }, + "drainUntil": { "type": "string", "format": "date-time" } + }, + "required": ["transition"] + }, + { + "type": "object", + "properties": { + "transition": { "type": "string", "enum": ["cancel"] } + }, + "required": ["transition"] + } + ] + } + } + } + }, + "responses": { + "200": { + "description": "200", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "espId": { "type": "string" }, + "name": { "type": "string" }, + "provider": { "type": "string" }, + "host": { "type": "string" }, + "port": { "type": "number" }, + "secure": { "type": "boolean" }, + "username": { "type": "string", "nullable": true }, + "hasPassword": { "type": "boolean" }, + "fromName": { "type": "string", "nullable": true }, + "fromEmail": { "type": "string", "nullable": true }, + "status": { + "type": "string", + "enum": ["draft", "active", "suspended", "draining", "retired"] + }, + "secretVersion": { + "type": "integer", + "minimum": 0, + "exclusiveMinimum": 0 + }, + "lastTestedAt": { "type": "string", "nullable": true }, + "lastTestStatus": { + "type": "string", + "enum": ["success", "failed"], + "nullable": true + }, + "lastTestError": { "type": "string", "nullable": true }, + "activatedAt": { "type": "string", "nullable": true }, + "drainUntil": { "type": "string", "nullable": true }, + "retiredAt": { "type": "string", "nullable": true }, + "updatedAt": { "type": "string" } + }, + "required": [ + "espId", + "name", + "provider", + "host", + "port", + "secure", + "hasPassword", + "status", + "secretVersion" + ] + } + } + } + }, + "403": { + "description": "403", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + }, + "404": { + "description": "404", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + }, + "409": { + "description": "409", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + } + } + } + }, + "/organizations/{organizationId}/esps/{espId}/feedback": { + "get": { + "summary": "Get organization ESP feedback configuration", + "tags": ["Organizations"], + "parameters": [ + { + "name": "organizationId", + "in": "path", + "required": true, + "schema": { "type": "string" } + }, + { + "name": "espId", + "in": "path", + "required": true, + "schema": { "type": "string" } + } + ], + "responses": { + "200": { + "description": "200", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "connectionId": { "type": "string" }, + "espId": { "type": "string" }, + "provider": { "type": "string" }, + "webhookUrl": { "type": "string" }, + "hasCredential": { "type": "boolean" }, + "status": { + "type": "string", + "enum": [ + "pending", + "healthy", + "stale", + "error", + "retiring", + "disabled" + ] + }, + "lastReceivedAt": { "type": "string", "nullable": true }, + "lastVerifiedAt": { "type": "string", "nullable": true }, + "lastErrorCode": { "type": "string", "nullable": true }, + "createdAt": { "type": "string" }, + "updatedAt": { "type": "string" } + }, + "required": [ + "connectionId", + "espId", + "provider", + "webhookUrl", + "hasCredential", + "status" + ], + "nullable": true + } + } + } + }, + "403": { + "description": "403", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + }, + "404": { + "description": "404", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + } + } + }, + "put": { + "summary": "Configure organization ESP feedback", + "tags": ["Organizations"], + "parameters": [ + { + "name": "organizationId", + "in": "path", + "required": true, + "schema": { "type": "string" } + }, + { + "name": "espId", + "in": "path", + "required": true, + "schema": { "type": "string" } + } + ], + "requestBody": { + "description": "Body", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "credential": { "type": "string", "minLength": 1 }, + "expectedTopicArn": { "type": "string" } + }, + "required": ["credential"] + } + } + } + }, + "responses": { + "200": { + "description": "200", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "connectionId": { "type": "string" }, + "espId": { "type": "string" }, + "provider": { "type": "string" }, + "webhookUrl": { "type": "string" }, + "hasCredential": { "type": "boolean" }, + "status": { + "type": "string", + "enum": [ + "pending", + "healthy", + "stale", + "error", + "retiring", + "disabled" + ] + }, + "lastReceivedAt": { "type": "string", "nullable": true }, + "lastVerifiedAt": { "type": "string", "nullable": true }, + "lastErrorCode": { "type": "string", "nullable": true }, + "createdAt": { "type": "string" }, + "updatedAt": { "type": "string" } + }, + "required": [ + "connectionId", + "espId", + "provider", + "webhookUrl", + "hasCredential", + "status" + ] + } + } + } + }, + "400": { + "description": "400", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + }, + "403": { + "description": "403", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + }, + "404": { + "description": "404", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + } + } + }, + "delete": { + "summary": "Disable organization ESP feedback configuration", + "tags": ["Organizations"], + "parameters": [ + { + "name": "organizationId", + "in": "path", + "required": true, + "schema": { "type": "string" } + }, + { + "name": "espId", + "in": "path", + "required": true, + "schema": { "type": "string" } + } + ], + "responses": { + "204": { "description": "204" }, + "403": { + "description": "403", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + }, + "404": { + "description": "404", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + } + } + } + }, + "/organizations/{organizationId}/esps/{espId}/feedback/rotate": { + "post": { + "summary": "Rotate organization ESP feedback credentials", + "tags": ["Organizations"], + "parameters": [ + { + "name": "organizationId", + "in": "path", + "required": true, + "schema": { "type": "string" } + }, + { + "name": "espId", + "in": "path", + "required": true, + "schema": { "type": "string" } + } + ], + "requestBody": { + "description": "Body", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "credential": { "type": "string", "minLength": 1 }, + "expectedTopicArn": { "type": "string" } + }, + "required": ["credential"] + } + } + } + }, + "responses": { + "200": { + "description": "200", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "connectionId": { "type": "string" }, + "espId": { "type": "string" }, + "provider": { "type": "string" }, + "webhookUrl": { "type": "string" }, + "hasCredential": { "type": "boolean" }, + "status": { + "type": "string", + "enum": [ + "pending", + "healthy", + "stale", + "error", + "retiring", + "disabled" + ] + }, + "lastReceivedAt": { "type": "string", "nullable": true }, + "lastVerifiedAt": { "type": "string", "nullable": true }, + "lastErrorCode": { "type": "string", "nullable": true }, + "createdAt": { "type": "string" }, + "updatedAt": { "type": "string" } + }, + "required": [ + "connectionId", + "espId", + "provider", + "webhookUrl", + "hasCredential", + "status" + ] + } + } + } + }, + "403": { + "description": "403", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + }, + "404": { + "description": "404", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + } + } + } + }, + "/organizations/{organizationId}/esps/{espId}/feedback/test": { + "post": { + "summary": "Verify organization ESP feedback configuration", + "tags": ["Organizations"], + "parameters": [ + { + "name": "organizationId", + "in": "path", + "required": true, + "schema": { "type": "string" } + }, + { + "name": "espId", + "in": "path", + "required": true, + "schema": { "type": "string" } + } + ], + "responses": { + "200": { + "description": "200", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { "type": "boolean" }, + "error": { "type": "string" } + }, + "required": ["success"] + } + } + } + }, + "403": { + "description": "403", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + }, + "404": { + "description": "404", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + } + } + } + }, + "/organizations/{organizationId}/delivery-policy": { + "get": { + "summary": "Get organization delivery policy", + "tags": ["Organizations"], + "parameters": [ + { + "name": "organizationId", + "in": "path", + "required": true, + "schema": { "type": "string" } + } + ], + "responses": { + "200": { + "description": "200", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "defaultEspId": { "type": "string", "nullable": true }, + "autoGrantDefaultEsp": { "type": "boolean" }, + "defaultDailyLimit": { + "type": "integer", + "minimum": 0, + "nullable": true + }, + "defaultMonthlyLimit": { + "type": "integer", + "minimum": 0, + "nullable": true + }, + "aggregateDailyLimit": { + "type": "integer", + "minimum": 0, + "nullable": true + }, + "aggregateMonthlyLimit": { + "type": "integer", + "minimum": 0, + "nullable": true + }, + "teamEspEnabledByDefault": { "type": "boolean" }, + "teamCanChangeDefault": { "type": "boolean" }, + "updatedAt": { "type": "string" } + }, + "required": [ + "defaultEspId", + "autoGrantDefaultEsp", + "defaultDailyLimit", + "defaultMonthlyLimit", + "aggregateDailyLimit", + "aggregateMonthlyLimit", + "teamEspEnabledByDefault", + "teamCanChangeDefault", + "updatedAt" + ] + } + } + } + }, + "403": { + "description": "403", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + }, + "404": { + "description": "404", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + } + } + }, + "put": { + "summary": "Update organization delivery policy", + "tags": ["Organizations"], + "parameters": [ + { + "name": "organizationId", + "in": "path", + "required": true, + "schema": { "type": "string" } + } + ], + "requestBody": { + "description": "Body", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "defaultEspId": { "type": "string", "nullable": true }, + "autoGrantDefaultEsp": { "type": "boolean" }, + "defaultDailyLimit": { + "type": "integer", + "minimum": 0, + "nullable": true + }, + "defaultMonthlyLimit": { + "type": "integer", + "minimum": 0, + "nullable": true + }, + "aggregateDailyLimit": { + "type": "integer", + "minimum": 0, + "nullable": true + }, + "aggregateMonthlyLimit": { + "type": "integer", + "minimum": 0, + "nullable": true + }, + "teamEspEnabledByDefault": { "type": "boolean" }, + "teamCanChangeDefault": { "type": "boolean" } + } + } + } + } + }, + "responses": { + "200": { + "description": "200", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "defaultEspId": { "type": "string", "nullable": true }, + "autoGrantDefaultEsp": { "type": "boolean" }, + "defaultDailyLimit": { + "type": "integer", + "minimum": 0, + "nullable": true + }, + "defaultMonthlyLimit": { + "type": "integer", + "minimum": 0, + "nullable": true + }, + "aggregateDailyLimit": { + "type": "integer", + "minimum": 0, + "nullable": true + }, + "aggregateMonthlyLimit": { + "type": "integer", + "minimum": 0, + "nullable": true + }, + "teamEspEnabledByDefault": { "type": "boolean" }, + "teamCanChangeDefault": { "type": "boolean" }, + "updatedAt": { "type": "string" } + }, + "required": [ + "defaultEspId", + "autoGrantDefaultEsp", + "defaultDailyLimit", + "defaultMonthlyLimit", + "aggregateDailyLimit", + "aggregateMonthlyLimit", + "teamEspEnabledByDefault", + "teamCanChangeDefault", + "updatedAt" + ] + } + } + } + }, + "403": { + "description": "403", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + }, + "404": { + "description": "404", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + }, + "422": { + "description": "422", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + } + } + } + }, + "/organizations/{organizationId}/usage": { + "get": { + "summary": "Get aggregate organization delivery quota usage", + "tags": ["Organizations"], + "parameters": [ + { + "name": "organizationId", + "in": "path", + "required": true, + "schema": { "type": "string" } + } + ], + "responses": { + "200": { + "description": "200", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "day": { + "type": "object", + "properties": { + "limit": { "type": "integer", "nullable": true }, + "accepted": { "type": "integer", "minimum": 0 }, + "reserved": { "type": "integer", "minimum": 0 }, + "remaining": { "type": "integer", "nullable": true }, + "resetsAt": { "type": "string" } + }, + "required": [ + "limit", + "accepted", + "reserved", + "remaining", + "resetsAt" + ] + }, + "month": { + "type": "object", + "properties": { + "limit": { "type": "integer", "nullable": true }, + "accepted": { "type": "integer", "minimum": 0 }, + "reserved": { "type": "integer", "minimum": 0 }, + "remaining": { "type": "integer", "nullable": true }, + "resetsAt": { "type": "string" } + }, + "required": [ + "limit", + "accepted", + "reserved", + "remaining", + "resetsAt" + ] + } + }, + "required": ["day", "month"] + } + } + } + }, + "403": { + "description": "403", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + }, + "404": { + "description": "404", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + } + } + } + }, + "/organizations/{organizationId}/mail-activity": { + "get": { + "description": "Returns per-team and organization-total transactional mail counts (`sent`, `queued`, `failed`, `bounced`) for a window of 1, 3, 7, or 30 days (default 7). Includes teams with zero activity. Does not include campaigns, broadcasts, sequences, recipients, subjects, or email bodies. Shared-delivery quota remains on `/usage`.", + "summary": "Get organization transactional mail activity", + "tags": ["Organizations"], + "parameters": [ + { + "name": "organizationId", + "in": "path", + "required": true, + "schema": { "type": "string" } + }, + { + "name": "rangeDays", + "in": "query", + "schema": { "type": "integer", "nullable": true } + } + ], + "responses": { + "200": { + "description": "200", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "rangeDays": { "type": "number", "enum": [1, 3, 7, 30] }, + "totals": { + "type": "object", + "properties": { + "sent": { "type": "integer", "minimum": 0 }, + "queued": { "type": "integer", "minimum": 0 }, + "failed": { "type": "integer", "minimum": 0 }, + "bounced": { "type": "integer", "minimum": 0 } + }, + "required": ["sent", "queued", "failed", "bounced"] + }, + "teams": { + "type": "array", + "items": { + "type": "object", + "properties": { + "teamId": { "type": "string" }, + "name": { "type": "string" }, + "status": { + "type": "string", + "enum": ["active", "sending_suspended", "archived"] + }, + "externalId": { "type": "string", "nullable": true }, + "mail": { + "type": "object", + "properties": { + "sent": { "type": "integer", "minimum": 0 }, + "queued": { "type": "integer", "minimum": 0 }, + "failed": { "type": "integer", "minimum": 0 }, + "bounced": { "type": "integer", "minimum": 0 } + }, + "required": ["sent", "queued", "failed", "bounced"] + } + }, + "required": ["teamId", "name", "status", "externalId", "mail"] + } + } + }, + "required": ["rangeDays", "totals", "teams"] + } + } + } + }, + "400": { + "description": "400", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + }, + "403": { + "description": "403", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + }, + "404": { + "description": "404", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + } + } + } + }, + "/organizations/{organizationId}/teams/{teamId}/enter": { + "post": { + "description": "Grants the calling human organization owner or administrator a permanent team `admin` membership, recorded as organization audit action `team.entered`. Organization API keys cannot enter. Archived teams cannot be entered. Idempotent if membership already exists.", + "summary": "Enter an organization team as a human admin", + "tags": ["Organizations"], + "parameters": [ + { + "name": "organizationId", + "in": "path", + "required": true, + "schema": { "type": "string" } + }, + { + "name": "teamId", + "in": "path", + "required": true, + "schema": { "type": "string" } + } + ], + "requestBody": { + "description": "Body", + "content": { + "application/json": { "schema": { "type": "object", "properties": {} } } + } + }, + "responses": { + "200": { + "description": "200", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "teamId": { "type": "string" }, + "role": { "type": "string", "enum": ["admin", "member"] }, + "created": { "type": "boolean" } + }, + "required": ["teamId", "role", "created"] + } + } + } + }, + "403": { + "description": "403", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + }, + "404": { + "description": "404", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + }, + "422": { + "description": "422", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + } + } + } + }, + "/organizations/{organizationId}/audit-events": { + "get": { + "summary": "List recent organization audit events", + "tags": ["Organizations"], + "parameters": [ + { + "name": "organizationId", + "in": "path", + "required": true, + "schema": { "type": "string" } + } + ], + "responses": { + "200": { + "description": "200", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "type": "object", + "properties": { + "action": { "type": "string" }, + "actorType": { + "type": "string", + "enum": ["user", "organization_key", "team_key", "system"] + }, + "teamId": { "type": "string", "nullable": true }, + "espId": { "type": "string", "nullable": true }, + "grantId": { "type": "string", "nullable": true }, + "metadata": { + "type": "object", + "additionalProperties": { "nullable": true } + }, + "createdAt": { "type": "string" } + }, + "required": [ + "action", + "actorType", + "teamId", + "espId", + "grantId", + "metadata", + "createdAt" + ] + } + } + }, + "required": ["items"] + } + } + } + }, + "403": { + "description": "403", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + }, + "404": { + "description": "404", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + } + } + } + }, + "/organizations/{organizationId}/teams/{teamId}/esp-grant": { + "get": { + "summary": "Get a team's organization ESP grant", + "tags": ["Organizations"], + "parameters": [ + { + "name": "organizationId", + "in": "path", + "required": true, + "schema": { "type": "string" } + }, + { + "name": "teamId", + "in": "path", + "required": true, + "schema": { "type": "string" } + } + ], + "responses": { + "200": { + "description": "200", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "grantId": { "type": "string" }, + "teamId": { "type": "string" }, + "espId": { "type": "string" }, + "status": { + "type": "string", + "enum": ["active", "draining", "suspended", "revoked"] + }, + "drainUntil": { "type": "string", "nullable": true }, + "fromName": { "type": "string", "nullable": true }, + "replyTo": { "type": "string", "nullable": true }, + "dailyLimit": { "type": "integer", "minimum": 0, "nullable": true }, + "monthlyLimit": { + "type": "integer", + "minimum": 0, + "nullable": true + }, + "createdAt": { "type": "string" }, + "updatedAt": { "type": "string" } + }, + "required": [ + "grantId", + "teamId", + "espId", + "status", + "drainUntil", + "fromName", + "replyTo", + "dailyLimit", + "monthlyLimit", + "createdAt", + "updatedAt" + ], + "nullable": true + } + } + } + }, + "403": { + "description": "403", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + }, + "404": { + "description": "404", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + } + } + }, + "put": { + "summary": "Create or update a team's organization ESP grant", + "tags": ["Organizations"], + "parameters": [ + { + "name": "organizationId", + "in": "path", + "required": true, + "schema": { "type": "string" } + }, + { + "name": "teamId", + "in": "path", + "required": true, + "schema": { "type": "string" } + } + ], + "requestBody": { + "description": "Body", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "espId": { "type": "string" }, + "fromName": { "type": "string", "nullable": true }, + "replyTo": { "type": "string", "format": "email", "nullable": true }, + "dailyLimit": { "type": "integer", "minimum": 0, "nullable": true }, + "monthlyLimit": { "type": "integer", "minimum": 0, "nullable": true }, + "makeDefault": { "type": "boolean" } + }, + "required": ["espId"] + } + } + } + }, + "responses": { + "200": { + "description": "200", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "grantId": { "type": "string" }, + "teamId": { "type": "string" }, + "espId": { "type": "string" }, + "status": { + "type": "string", + "enum": ["active", "draining", "suspended", "revoked"] + }, + "drainUntil": { "type": "string", "nullable": true }, + "fromName": { "type": "string", "nullable": true }, + "replyTo": { "type": "string", "nullable": true }, + "dailyLimit": { "type": "integer", "minimum": 0, "nullable": true }, + "monthlyLimit": { + "type": "integer", + "minimum": 0, + "nullable": true + }, + "createdAt": { "type": "string" }, + "updatedAt": { "type": "string" } + }, + "required": [ + "grantId", + "teamId", + "espId", + "status", + "drainUntil", + "fromName", + "replyTo", + "dailyLimit", + "monthlyLimit", + "createdAt", + "updatedAt" + ] + } + } + } + }, + "403": { + "description": "403", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + }, + "404": { + "description": "404", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + }, + "409": { + "description": "409", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + }, + "422": { + "description": "422", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + } + } + } + }, + "/organizations/{organizationId}/teams/{teamId}/esp-grant/transition": { + "post": { + "summary": "Transition a team's organization ESP grant; revoking detaches safe campaigns", + "tags": ["Organizations"], + "parameters": [ + { + "name": "organizationId", + "in": "path", + "required": true, + "schema": { "type": "string" } + }, + { + "name": "teamId", + "in": "path", + "required": true, + "schema": { "type": "string" } + } + ], + "requestBody": { + "description": "Body", + "content": { + "application/json": { + "schema": { + "discriminator": { "propertyName": "action" }, + "oneOf": [ + { + "type": "object", + "properties": { + "action": { "type": "string", "enum": ["suspend"] } + }, + "required": ["action"] + }, + { + "type": "object", + "properties": { + "action": { "type": "string", "enum": ["resume"] } + }, + "required": ["action"] + }, + { + "type": "object", + "properties": { + "action": { "type": "string", "enum": ["drain"] }, + "drainUntil": { "type": "string", "format": "date-time" } + }, + "required": ["action"] + }, + { + "type": "object", + "properties": { + "action": { "type": "string", "enum": ["cancel"] } + }, + "required": ["action"] + } + ] + } + } + } + }, + "responses": { + "200": { + "description": "200", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "grantId": { "type": "string" }, + "teamId": { "type": "string" }, + "espId": { "type": "string" }, + "status": { + "type": "string", + "enum": ["active", "draining", "suspended", "revoked"] + }, + "drainUntil": { "type": "string", "nullable": true }, + "fromName": { "type": "string", "nullable": true }, + "replyTo": { "type": "string", "nullable": true }, + "dailyLimit": { "type": "integer", "minimum": 0, "nullable": true }, + "monthlyLimit": { + "type": "integer", + "minimum": 0, + "nullable": true + }, + "createdAt": { "type": "string" }, + "updatedAt": { "type": "string" } + }, + "required": [ + "grantId", + "teamId", + "espId", + "status", + "drainUntil", + "fromName", + "replyTo", + "dailyLimit", + "monthlyLimit", + "createdAt", + "updatedAt" + ] + } + } + } + }, + "403": { + "description": "403", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + }, + "404": { + "description": "404", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + }, + "409": { + "description": "409", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + } + } + } + }, + "/organizations/{organizationId}/sending-domains": { + "get": { + "summary": "List verified sending domains", + "tags": ["Organizations"], + "parameters": [ + { + "name": "organizationId", + "in": "path", + "required": true, + "schema": { "type": "string" } + } + ], + "responses": { + "200": { + "description": "200", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "type": "object", + "properties": { + "domainId": { "type": "string" }, + "domain": { "type": "string" }, + "status": { + "type": "string", + "enum": ["pending", "verified", "revoked", "failed"] + }, + "verifiedAt": { "type": "string", "nullable": true }, + "lastCheckedAt": { "type": "string", "nullable": true }, + "nextCheckAt": { "type": "string", "nullable": true }, + "challengeToken": { "type": "string", "nullable": true }, + "challengeRecordName": { "type": "string", "nullable": true }, + "challengeRecordValue": { "type": "string", "nullable": true } + }, + "required": [ + "domainId", + "domain", + "status", + "verifiedAt", + "lastCheckedAt", + "nextCheckAt", + "challengeToken", + "challengeRecordName", + "challengeRecordValue" + ] + } + } + }, + "required": ["items"] + } + } + } + }, + "403": { + "description": "403", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + }, + "404": { + "description": "404", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + } + } + }, + "post": { + "summary": "Create a DNS verification challenge for a sending domain", + "tags": ["Organizations"], + "parameters": [ + { + "name": "organizationId", + "in": "path", + "required": true, + "schema": { "type": "string" } + } + ], + "requestBody": { + "description": "Body", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "domain": { "type": "string", "minLength": 1, "maxLength": 253 } + }, + "required": ["domain"] + } + } + } + }, + "responses": { + "201": { + "description": "201", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "domainId": { "type": "string" }, + "domain": { "type": "string" }, + "status": { + "type": "string", + "enum": ["pending", "verified", "revoked", "failed"] + }, + "verifiedAt": { "type": "string", "nullable": true }, + "lastCheckedAt": { "type": "string", "nullable": true }, + "nextCheckAt": { "type": "string", "nullable": true }, + "challengeToken": { "type": "string", "nullable": true }, + "challengeRecordName": { "type": "string", "nullable": true }, + "challengeRecordValue": { "type": "string", "nullable": true } + }, + "required": [ + "domainId", + "domain", + "status", + "verifiedAt", + "lastCheckedAt", + "nextCheckAt", + "challengeToken", + "challengeRecordName", + "challengeRecordValue" + ] + } + } + } + }, + "400": { + "description": "400", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + }, + "403": { + "description": "403", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + }, + "404": { + "description": "404", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + }, + "409": { + "description": "409", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + } + } + } + }, + "/organizations/{organizationId}/sending-domains/{domainId}/verify": { + "post": { + "summary": "Refresh DNS verification for a sending domain", + "tags": ["Organizations"], + "parameters": [ + { + "name": "organizationId", + "in": "path", + "required": true, + "schema": { "type": "string" } + }, + { + "name": "domainId", + "in": "path", + "required": true, + "schema": { "type": "string" } + } + ], + "responses": { + "200": { + "description": "200", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "domainId": { "type": "string" }, + "domain": { "type": "string" }, + "status": { + "type": "string", + "enum": ["pending", "verified", "revoked", "failed"] + }, + "verifiedAt": { "type": "string", "nullable": true }, + "lastCheckedAt": { "type": "string", "nullable": true }, + "nextCheckAt": { "type": "string", "nullable": true }, + "challengeToken": { "type": "string", "nullable": true }, + "challengeRecordName": { "type": "string", "nullable": true }, + "challengeRecordValue": { "type": "string", "nullable": true } + }, + "required": [ + "domainId", + "domain", + "status", + "verifiedAt", + "lastCheckedAt", + "nextCheckAt", + "challengeToken", + "challengeRecordName", + "challengeRecordValue" + ] + } + } + } + }, + "403": { + "description": "403", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + }, + "404": { + "description": "404", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + }, + "422": { + "description": "422", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + } + } + } + }, + "/organizations/{organizationId}/sending-domains/{domainId}": { + "delete": { + "summary": "Revoke a sending domain", + "tags": ["Organizations"], + "parameters": [ + { + "name": "organizationId", + "in": "path", + "required": true, + "schema": { "type": "string" } + }, + { + "name": "domainId", + "in": "path", + "required": true, + "schema": { "type": "string" } + } + ], + "responses": { + "204": { "description": "204" }, + "403": { + "description": "403", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + }, + "404": { + "description": "404", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + } + } + } + }, + "/sending-options": { + "get": { + "summary": "List sanitized sending options for the active team", + "tags": ["Delivery"], + "parameters": [], + "responses": { + "200": { + "description": "200", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "discriminator": { "propertyName": "type" }, + "oneOf": [ + { + "type": "object", + "properties": { + "type": { "type": "string", "enum": ["organization"] }, + "name": { "type": "string" }, + "fromName": { "type": "string", "nullable": true }, + "fromEmail": { "type": "string", "nullable": true }, + "replyTo": { "type": "string", "nullable": true }, + "isDefault": { "type": "boolean" }, + "available": { "type": "boolean" }, + "countsAgainstQuota": { + "type": "boolean", + "enum": [true] + } + }, + "required": [ + "type", + "name", + "fromName", + "fromEmail", + "replyTo", + "isDefault", + "available", + "countsAgainstQuota" + ] + }, + { + "type": "object", + "properties": { + "type": { "type": "string", "enum": ["team"] }, + "espId": { "type": "string" }, + "name": { "type": "string" }, + "fromName": { "type": "string", "nullable": true }, + "fromEmail": { "type": "string", "nullable": true }, + "isDefault": { "type": "boolean" }, + "available": { "type": "boolean" }, + "countsAgainstQuota": { + "type": "boolean", + "enum": [false] + } + }, + "required": [ + "type", + "espId", + "name", + "fromName", + "fromEmail", + "isDefault", + "available", + "countsAgainstQuota" + ] + } + ] + } + } + }, + "required": ["items"] + } + } + } + } + } + } + }, + "/settings/delivery": { + "get": { + "summary": "Get team delivery settings", + "tags": ["Delivery"], + "parameters": [], + "responses": { + "200": { + "description": "200", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "teamEspEnabled": { "type": "boolean" }, + "teamCanChangeDefault": { "type": "boolean" }, + "defaultSource": { + "type": "string", + "enum": ["organization", "team"], + "nullable": true + }, + "defaultTeamEspId": { "type": "string", "nullable": true }, + "updatedAt": { "type": "string" } + }, + "required": [ + "teamEspEnabled", + "teamCanChangeDefault", + "defaultSource", + "defaultTeamEspId", + "updatedAt" + ] + } + } + } + } + } + }, + "patch": { + "summary": "Choose the team's default delivery source", + "tags": ["Delivery"], + "parameters": [], + "requestBody": { + "description": "Body", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "deliverySource": { + "discriminator": { "propertyName": "type" }, + "oneOf": [ + { + "type": "object", + "properties": { + "type": { "type": "string", "enum": ["organization"] } + }, + "required": ["type"] + }, + { + "type": "object", + "properties": { + "type": { "type": "string", "enum": ["team"] }, + "espId": { "type": "string" } + }, + "required": ["type", "espId"] + } + ] + } + }, + "required": ["deliverySource"] + } + } + } + }, + "responses": { + "200": { + "description": "200", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "teamEspEnabled": { "type": "boolean" }, + "teamCanChangeDefault": { "type": "boolean" }, + "defaultSource": { + "type": "string", + "enum": ["organization", "team"], + "nullable": true + }, + "defaultTeamEspId": { "type": "string", "nullable": true }, + "updatedAt": { "type": "string" } + }, + "required": [ + "teamEspEnabled", + "teamCanChangeDefault", + "defaultSource", + "defaultTeamEspId", + "updatedAt" + ] + } + } + } + }, + "403": { + "description": "403", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + }, + "409": { + "description": "409", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + }, + "422": { + "description": "422", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + } + } + } + }, + "/provisioning/teams": { + "post": { + "description": "Server-to-server endpoint for multi-tenant consumers (e.g. CourseLit) to provision one SendLit team per tenant. Requires a scoped organization key as a Bearer token.", + "summary": "Find-or-create a team for a consumer-supplied tenant id", + "tags": ["Teams"], + "parameters": [], + "requestBody": { + "description": "Body", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "externalId": { "type": "string", "minLength": 1 }, + "name": { "type": "string", "minLength": 1 }, + "sender": { + "type": "object", + "properties": { + "fromName": { "type": "string", "minLength": 1 }, + "replyTo": { "type": "string", "format": "email" } + } + }, + "mailingAddress": { "type": "string", "minLength": 1 }, + "delivery": { + "type": "object", + "properties": { + "useOrganizationDefault": { "type": "boolean" }, + "teamEspEnabled": { "type": "boolean" }, + "teamCanChangeDefault": { "type": "boolean" } + } + }, + "quota": { + "type": "object", + "properties": { + "dailyLimit": { + "type": "integer", + "minimum": 0, + "exclusiveMinimum": 0, + "nullable": true + }, + "monthlyLimit": { + "type": "integer", + "minimum": 0, + "exclusiveMinimum": 0, + "nullable": true + } + } + } + }, + "required": ["externalId", "name"] + } + } + } + }, + "responses": { + "200": { + "description": "200", + "content": { + "application/json": { + "schema": { + "discriminator": { "propertyName": "created" }, + "oneOf": [ + { + "type": "object", + "properties": { + "teamId": { "type": "string" }, + "externalId": { "type": "string" }, + "name": { "type": "string" }, + "deliverySource": { + "type": "object", + "properties": { + "type": { "type": "string", "enum": ["organization"] } + }, + "required": ["type"] + }, + "created": { "type": "boolean", "enum": [true] }, + "apiKey": { "type": "string" } + }, + "required": [ + "teamId", + "externalId", + "name", + "deliverySource", + "created", + "apiKey" + ] + }, + { + "type": "object", + "properties": { + "teamId": { "type": "string" }, + "externalId": { "type": "string" }, + "name": { "type": "string" }, + "deliverySource": { + "type": "object", + "properties": { + "type": { "type": "string", "enum": ["organization"] } + }, + "required": ["type"] + }, + "created": { "type": "boolean", "enum": [false] }, + "apiKey": { + "type": "string", + "format": "null", + "nullable": true + } + }, + "required": [ + "teamId", + "externalId", + "name", + "deliverySource", + "created", + "apiKey" + ] + } + ] + } + } + } + }, + "400": { + "description": "400", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + }, + "401": { + "description": "401", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + }, + "402": { + "description": "402", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + }, + "403": { + "description": "403", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + }, + "409": { + "description": "409", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + }, + "500": { + "description": "500", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + } + } + } + }, + "/provisioning/teams/{teamId}": { + "get": { + "summary": "Get a provisioned team within the authenticated organization", + "tags": ["Teams"], + "parameters": [ + { + "name": "teamId", + "in": "path", + "required": true, + "schema": { "type": "string" } + } + ], + "responses": { + "200": { + "description": "200", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "teamId": { "type": "string" }, + "externalId": { "type": "string" }, + "name": { "type": "string" }, + "deliverySource": { + "type": "object", + "properties": { + "type": { "type": "string", "enum": ["organization"] } + }, + "required": ["type"] + }, + "status": { + "type": "string", + "enum": ["active", "sending_suspended", "archived"] + }, + "mailingAddress": { "type": "string", "nullable": true }, + "teamEspEnabled": { "type": "boolean" }, + "teamCanChangeDefault": { "type": "boolean" }, + "createdAt": { "type": "string", "nullable": true }, + "updatedAt": { "type": "string", "nullable": true } + }, + "required": [ + "teamId", + "externalId", + "name", + "deliverySource", + "status", + "mailingAddress", + "teamEspEnabled", + "teamCanChangeDefault", + "createdAt", + "updatedAt" + ] + } + } + } + }, + "403": { + "description": "403", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + }, + "404": { + "description": "404", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + } + } + }, + "patch": { + "summary": "Update a provisioned team's sender, limits, or settings", + "tags": ["Teams"], + "parameters": [ + { + "name": "teamId", + "in": "path", + "required": true, + "schema": { "type": "string" } + } + ], + "requestBody": { + "description": "Body", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "name": { "type": "string", "minLength": 1 }, + "sender": { + "type": "object", + "properties": { + "fromName": { + "type": "string", + "minLength": 1, + "nullable": true + }, + "replyTo": { + "type": "string", + "format": "email", + "nullable": true + } + } + }, + "mailingAddress": { + "type": "string", + "minLength": 1, + "nullable": true + }, + "delivery": { + "type": "object", + "properties": { + "teamEspEnabled": { "type": "boolean" }, + "teamCanChangeDefault": { "type": "boolean" } + } + }, + "quota": { + "type": "object", + "properties": { + "dailyLimit": { + "type": "integer", + "minimum": 0, + "nullable": true + }, + "monthlyLimit": { + "type": "integer", + "minimum": 0, + "nullable": true + } + } + } + } + } + } + } + }, + "responses": { + "200": { + "description": "200", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "teamId": { "type": "string" }, + "externalId": { "type": "string" }, + "name": { "type": "string" }, + "deliverySource": { + "type": "object", + "properties": { + "type": { "type": "string", "enum": ["organization"] } + }, + "required": ["type"] + }, + "status": { + "type": "string", + "enum": ["active", "sending_suspended", "archived"] + }, + "mailingAddress": { "type": "string", "nullable": true }, + "teamEspEnabled": { "type": "boolean" }, + "teamCanChangeDefault": { "type": "boolean" }, + "createdAt": { "type": "string", "nullable": true }, + "updatedAt": { "type": "string", "nullable": true } + }, + "required": [ + "teamId", + "externalId", + "name", + "deliverySource", + "status", + "mailingAddress", + "teamEspEnabled", + "teamCanChangeDefault", + "createdAt", + "updatedAt" + ] + } + } + } + }, + "403": { + "description": "403", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + }, + "404": { + "description": "404", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + }, + "409": { + "description": "409", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + } + } + }, + "delete": { + "summary": "Archive a provisioned team", + "tags": ["Teams"], + "parameters": [ + { + "name": "teamId", + "in": "path", + "required": true, + "schema": { "type": "string" } + } + ], + "responses": { + "204": { "description": "204" }, + "403": { + "description": "403", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + }, + "404": { + "description": "404", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + } + } + } + }, + "/provisioning/teams/{teamId}/keys": { + "post": { + "summary": "Replace a provisioned team's integration key", + "tags": ["Teams"], + "parameters": [ + { + "name": "teamId", + "in": "path", + "required": true, + "schema": { "type": "string" } + } + ], + "requestBody": { + "description": "Body", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "name": { "type": "string", "minLength": 1 } }, + "required": ["name"] + } + } + } + }, + "responses": { + "201": { + "description": "201", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "keyId": { "type": "string" }, + "keyPrefix": { "type": "string" }, + "name": { "type": "string" }, + "expiresAt": { "type": "string", "nullable": true }, + "lastUsedAt": { "type": "string", "nullable": true }, + "revokedAt": { "type": "string", "nullable": true }, + "createdAt": { "type": "string", "nullable": true }, + "key": { "type": "string" } + }, + "required": ["keyId", "keyPrefix", "name", "key"] + } + } + } + }, + "403": { + "description": "403", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + }, + "404": { + "description": "404", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + } + } + } + }, + "/provisioning/teams/{teamId}/suspend": { + "post": { + "summary": "Suspend new sends for a provisioned team", + "tags": ["Teams"], + "parameters": [ + { + "name": "teamId", + "in": "path", + "required": true, + "schema": { "type": "string" } + } + ], + "responses": { + "200": { + "description": "200", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "teamId": { "type": "string" }, + "externalId": { "type": "string" }, + "name": { "type": "string" }, + "deliverySource": { + "type": "object", + "properties": { + "type": { "type": "string", "enum": ["organization"] } + }, + "required": ["type"] + }, + "status": { + "type": "string", + "enum": ["active", "sending_suspended", "archived"] + }, + "mailingAddress": { "type": "string", "nullable": true }, + "teamEspEnabled": { "type": "boolean" }, + "teamCanChangeDefault": { "type": "boolean" }, + "createdAt": { "type": "string", "nullable": true }, + "updatedAt": { "type": "string", "nullable": true } + }, + "required": [ + "teamId", + "externalId", + "name", + "deliverySource", + "status", + "mailingAddress", + "teamEspEnabled", + "teamCanChangeDefault", + "createdAt", + "updatedAt" + ] + } + } + } + }, + "403": { + "description": "403", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + }, + "404": { + "description": "404", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + }, + "409": { + "description": "409", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + } + } + } + }, + "/provisioning/teams/{teamId}/resume": { + "post": { + "summary": "Resume sends for a provisioned team", + "tags": ["Teams"], + "parameters": [ + { + "name": "teamId", + "in": "path", + "required": true, + "schema": { "type": "string" } + } + ], + "responses": { + "200": { + "description": "200", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "teamId": { "type": "string" }, + "externalId": { "type": "string" }, + "name": { "type": "string" }, + "deliverySource": { + "type": "object", + "properties": { + "type": { "type": "string", "enum": ["organization"] } + }, + "required": ["type"] + }, + "status": { + "type": "string", + "enum": ["active", "sending_suspended", "archived"] + }, + "mailingAddress": { "type": "string", "nullable": true }, + "teamEspEnabled": { "type": "boolean" }, + "teamCanChangeDefault": { "type": "boolean" }, + "createdAt": { "type": "string", "nullable": true }, + "updatedAt": { "type": "string", "nullable": true } + }, + "required": [ + "teamId", + "externalId", + "name", + "deliverySource", + "status", + "mailingAddress", + "teamEspEnabled", + "teamCanChangeDefault", + "createdAt", + "updatedAt" + ] + } + } + } + }, + "403": { + "description": "403", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + }, + "404": { + "description": "404", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + }, + "409": { + "description": "409", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + } + } + } + }, + "/provisioning/teams/{teamId}/usage": { + "get": { + "summary": "Get organization-delivery quota usage for a provisioned team", + "tags": ["Teams"], + "parameters": [ + { + "name": "teamId", + "in": "path", + "required": true, + "schema": { "type": "string" } + } + ], + "responses": { + "200": { + "description": "200", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "day": { + "type": "object", + "properties": { + "limit": { "type": "integer", "nullable": true }, + "accepted": { "type": "integer", "minimum": 0 }, + "reserved": { "type": "integer", "minimum": 0 }, + "remaining": { "type": "integer", "nullable": true }, + "resetsAt": { "type": "string" } + }, + "required": [ + "limit", + "accepted", + "reserved", + "remaining", + "resetsAt" + ] + }, + "month": { + "type": "object", + "properties": { + "limit": { "type": "integer", "nullable": true }, + "accepted": { "type": "integer", "minimum": 0 }, + "reserved": { "type": "integer", "minimum": 0 }, + "remaining": { "type": "integer", "nullable": true }, + "resetsAt": { "type": "string" } + }, + "required": [ + "limit", + "accepted", + "reserved", + "remaining", + "resetsAt" + ] + } + }, + "required": ["day", "month"] + } + } + } + }, + "403": { + "description": "403", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + }, + "404": { + "description": "404", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + } + } + } + }, + "/overview": { + "get": { + "description": "Use `rangeDays` (1, 3, 7, or 30; default 7) to filter transactional delivery activity and scheduled broadcasts.", + "summary": "Get team overview metrics for a delivery activity window", + "tags": ["Overview"], + "parameters": [ + { + "name": "rangeDays", + "in": "query", + "schema": { "type": "integer", "nullable": true } + } + ], + "responses": { + "200": { + "description": "200", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "activeSequences": { "type": "number" }, + "ongoingContacts": { "type": "number" }, + "scheduledBroadcasts": { "type": "number" }, + "mail": { + "type": "object", + "properties": { + "sent": { "type": "number" }, + "queued": { "type": "number" }, + "failed": { "type": "number" }, + "bounced": { "type": "number" } + }, + "required": ["sent", "queued", "failed", "bounced"] + }, + "quota": { + "type": "object", + "properties": { + "dailyUsed": { "type": "number" }, + "dailyLimit": { "type": "number" }, + "monthlyUsed": { "type": "number" }, + "monthlyLimit": { "type": "number" } + }, + "required": [ + "dailyUsed", + "dailyLimit", + "monthlyUsed", + "monthlyLimit" + ] + } + }, + "required": [ + "activeSequences", + "ongoingContacts", + "scheduledBroadcasts", + "mail", + "quota" + ] + } + } + } + } + } + } + }, + "/settings/esps/{espId}/feedback": { + "get": { + "description": "Returns null when feedback hasn't been configured for this ESP yet.", + "summary": "Get a user ESP's delivery-feedback connection", + "tags": ["Settings"], + "parameters": [ + { + "name": "espId", + "in": "path", + "required": true, + "schema": { "type": "string" } + } + ], + "responses": { + "200": { + "description": "200", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "connectionId": { "type": "string" }, + "espId": { "type": "string" }, + "provider": { "type": "string" }, + "webhookUrl": { "type": "string" }, + "hasCredential": { "type": "boolean" }, + "status": { + "type": "string", + "enum": [ + "pending", + "healthy", + "stale", + "error", + "retiring", + "disabled" + ] + }, + "lastReceivedAt": { "type": "string", "nullable": true }, + "lastVerifiedAt": { "type": "string", "nullable": true }, + "lastErrorCode": { "type": "string", "nullable": true }, + "createdAt": { "type": "string" }, + "updatedAt": { "type": "string" } + }, + "required": [ + "connectionId", + "espId", + "provider", + "webhookUrl", + "hasCredential", + "status" + ], + "nullable": true + } + } + } + }, + "404": { + "description": "404", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + } + } + }, + "put": { + "description": "Creates the connection (and its stable webhook URL) on first call; a later call rotates the credential without changing the URL. The provider is always the ESP's current provider, not client-writable.", + "summary": "Create or rotate a user ESP's delivery-feedback connection", + "tags": ["Settings"], + "parameters": [ + { + "name": "espId", + "in": "path", + "required": true, + "schema": { "type": "string" } + } + ], + "requestBody": { + "description": "Body", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "credential": { "type": "string", "minLength": 1 }, + "expectedTopicArn": { "type": "string" } + }, + "required": ["credential"] + } + } + } + }, + "responses": { + "200": { + "description": "200", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "connectionId": { "type": "string" }, + "espId": { "type": "string" }, + "provider": { "type": "string" }, + "webhookUrl": { "type": "string" }, + "hasCredential": { "type": "boolean" }, + "status": { + "type": "string", + "enum": [ + "pending", + "healthy", + "stale", + "error", + "retiring", + "disabled" + ] + }, + "lastReceivedAt": { "type": "string", "nullable": true }, + "lastVerifiedAt": { "type": "string", "nullable": true }, + "lastErrorCode": { "type": "string", "nullable": true }, + "createdAt": { "type": "string" }, + "updatedAt": { "type": "string" } + }, + "required": [ + "connectionId", + "espId", + "provider", + "webhookUrl", + "hasCredential", + "status" + ] + } + } + } + }, + "400": { + "description": "400", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + }, + "404": { + "description": "404", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + } + } + }, + "delete": { + "summary": "Disable a user ESP's delivery-feedback connection", + "tags": ["Settings"], + "parameters": [ + { + "name": "espId", + "in": "path", + "required": true, + "schema": { "type": "string" } + } + ], + "responses": { + "204": { "description": "204" }, + "404": { + "description": "404", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + } + } + } + }, + "/settings/esps/{espId}/feedback/rotate": { + "post": { + "description": "Same effect as PUT, but fails with 404 if no connection exists yet. The previous credential remains valid for 24h so in-flight provider retries aren't lost.", + "summary": "Rotate an existing feedback connection's credential", + "tags": ["Settings"], + "parameters": [ + { + "name": "espId", + "in": "path", + "required": true, + "schema": { "type": "string" } + } + ], + "requestBody": { + "description": "Body", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "credential": { "type": "string", "minLength": 1 }, + "expectedTopicArn": { "type": "string" } + }, + "required": ["credential"] + } + } + } + }, + "responses": { + "200": { + "description": "200", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "connectionId": { "type": "string" }, + "espId": { "type": "string" }, + "provider": { "type": "string" }, + "webhookUrl": { "type": "string" }, + "hasCredential": { "type": "boolean" }, + "status": { + "type": "string", + "enum": [ + "pending", + "healthy", + "stale", + "error", + "retiring", + "disabled" + ] + }, + "lastReceivedAt": { "type": "string", "nullable": true }, + "lastVerifiedAt": { "type": "string", "nullable": true }, + "lastErrorCode": { "type": "string", "nullable": true }, + "createdAt": { "type": "string" }, + "updatedAt": { "type": "string" } + }, + "required": [ + "connectionId", + "espId", + "provider", + "webhookUrl", + "hasCredential", + "status" + ] + } + } + } + }, + "400": { + "description": "400", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + }, + "404": { + "description": "404", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + } + } + } + }, + "/settings/esps/{espId}/feedback/test": { + "post": { + "summary": "Verify a feedback connection's stored credential", + "tags": ["Settings"], + "parameters": [ + { + "name": "espId", + "in": "path", + "required": true, + "schema": { "type": "string" } + } + ], + "responses": { + "200": { + "description": "200", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { "type": "boolean" }, + "error": { "type": "string" } + }, + "required": ["success"] + } + } + } + }, + "404": { + "description": "404", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + } + } + } + }, + "/delivery-events": { + "get": { + "summary": "List normalized delivery events for the team", + "tags": ["Delivery"], + "parameters": [ + { "name": "espId", "in": "query", "schema": { "type": "string" } }, + { + "name": "deliverySourceType", + "in": "query", + "schema": { "type": "string", "enum": ["organization", "team"] } + }, + { + "name": "eventType", + "in": "query", + "schema": { + "type": "string", + "enum": [ + "accepted", + "delivered", + "delayed", + "soft_bounce", + "hard_bounce", + "failed", + "complaint", + "suppressed", + "rejected", + "unknown" + ] + } + }, + { + "name": "createdAfter", + "in": "query", + "schema": { "type": "integer", "nullable": true } + }, + { + "name": "createdBefore", + "in": "query", + "schema": { "type": "integer", "nullable": true } + }, + { + "name": "offset", + "in": "query", + "schema": { "type": "integer", "minimum": 1 } + }, + { + "name": "itemsPerPage", + "in": "query", + "schema": { "type": "integer", "minimum": 1 } + } + ], + "responses": { + "200": { + "description": "200", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "type": "object", + "properties": { + "eventId": { "type": "string" }, + "provider": { "type": "string" }, + "espId": { "type": "string", "nullable": true }, + "deliverySourceType": { + "type": "string", + "enum": ["organization", "team"], + "nullable": true + }, + "messageId": { "type": "string", "nullable": true }, + "recipientEmail": { "type": "string", "nullable": true }, + "eventType": { + "type": "string", + "enum": [ + "accepted", + "delivered", + "delayed", + "soft_bounce", + "hard_bounce", + "failed", + "complaint", + "suppressed", + "rejected", + "unknown" + ] + }, + "bounceClass": { + "type": "string", + "enum": ["permanent", "transient", "undetermined"], + "nullable": true + }, + "smtpCode": { "type": "number", "nullable": true }, + "enhancedStatusCode": { "type": "string", "nullable": true }, + "reason": { "type": "string", "nullable": true }, + "occurredAt": { "type": "string" }, + "receivedAt": { "type": "string" } + }, + "required": [ + "eventId", + "provider", + "espId", + "deliverySourceType", + "messageId", + "recipientEmail", + "eventType", + "occurredAt", + "receivedAt" + ] + } + }, + "total": { "type": "number" } + }, + "required": ["items", "total"] + } + } + } + } + } + } + }, + "/delivery-events/{eventId}": { + "get": { + "summary": "Get a single normalized delivery event", + "tags": ["Delivery"], + "parameters": [ + { + "name": "eventId", + "in": "path", + "required": true, + "schema": { "type": "string" } + } + ], + "responses": { + "200": { + "description": "200", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "eventId": { "type": "string" }, + "provider": { "type": "string" }, + "espId": { "type": "string", "nullable": true }, + "deliverySourceType": { + "type": "string", + "enum": ["organization", "team"], + "nullable": true + }, + "messageId": { "type": "string", "nullable": true }, + "recipientEmail": { "type": "string", "nullable": true }, + "eventType": { + "type": "string", + "enum": [ + "accepted", + "delivered", + "delayed", + "soft_bounce", + "hard_bounce", + "failed", + "complaint", + "suppressed", + "rejected", + "unknown" + ] + }, + "bounceClass": { + "type": "string", + "enum": ["permanent", "transient", "undetermined"], + "nullable": true + }, + "smtpCode": { "type": "number", "nullable": true }, + "enhancedStatusCode": { "type": "string", "nullable": true }, + "reason": { "type": "string", "nullable": true }, + "occurredAt": { "type": "string" }, + "receivedAt": { "type": "string" } + }, + "required": [ + "eventId", + "provider", + "espId", + "deliverySourceType", + "messageId", + "recipientEmail", + "eventType", + "occurredAt", + "receivedAt" + ] + } + } + } + }, + "404": { + "description": "404", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + } + } + } + }, + "/suppressions": { + "get": { + "summary": "List the team's suppressed recipients", + "tags": ["Delivery"], + "parameters": [ + { + "name": "active", + "in": "query", + "schema": { "type": "boolean", "nullable": true } + }, + { + "name": "reason", + "in": "query", + "schema": { + "type": "string", + "enum": [ + "hard_bounce", + "complaint", + "repeated_soft_bounce", + "provider_suppression", + "manual" + ] + } + }, + { + "name": "offset", + "in": "query", + "schema": { "type": "integer", "minimum": 1 } + }, + { + "name": "itemsPerPage", + "in": "query", + "schema": { "type": "integer", "minimum": 1 } + } + ], + "responses": { + "200": { + "description": "200", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "type": "object", + "properties": { + "suppressionId": { "type": "string" }, + "recipientEmail": { "type": "string", "nullable": true }, + "reason": { + "type": "string", + "enum": [ + "hard_bounce", + "complaint", + "repeated_soft_bounce", + "provider_suppression", + "manual" + ] + }, + "active": { "type": "boolean" }, + "firstSuppressedAt": { "type": "string" }, + "lastSuppressedAt": { "type": "string" }, + "releasedAt": { "type": "string", "nullable": true }, + "releaseReason": { "type": "string", "nullable": true } + }, + "required": [ + "suppressionId", + "recipientEmail", + "reason", + "active", + "firstSuppressedAt", + "lastSuppressedAt" + ] + } + }, + "total": { "type": "number" } + }, + "required": ["items", "total"] + } + } + } + } + } + } + }, + "/suppressions/{suppressionId}": { + "get": { + "summary": "Get a single suppression", + "tags": ["Delivery"], + "parameters": [ + { + "name": "suppressionId", + "in": "path", + "required": true, + "schema": { "type": "string" } + } + ], + "responses": { + "200": { + "description": "200", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "suppressionId": { "type": "string" }, + "recipientEmail": { "type": "string", "nullable": true }, + "reason": { + "type": "string", + "enum": [ + "hard_bounce", + "complaint", + "repeated_soft_bounce", + "provider_suppression", + "manual" + ] + }, + "active": { "type": "boolean" }, + "firstSuppressedAt": { "type": "string" }, + "lastSuppressedAt": { "type": "string" }, + "releasedAt": { "type": "string", "nullable": true }, + "releaseReason": { "type": "string", "nullable": true } + }, + "required": [ + "suppressionId", + "recipientEmail", + "reason", + "active", + "firstSuppressedAt", + "lastSuppressedAt" + ] + } + } + } + }, + "404": { + "description": "404", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + } + } + } + }, + "/suppressions/{suppressionId}/release": { + "post": { + "description": "A workspace owner may release hard_bounce, repeated_soft_bounce, and manual suppressions only. Complaint suppressions cannot be released through this endpoint (409 suppression_not_releasable) — see the PRD's reactivation policy.", + "summary": "Release an active suppression", + "tags": ["Delivery"], + "parameters": [ + { + "name": "suppressionId", + "in": "path", + "required": true, + "schema": { "type": "string" } + } + ], + "requestBody": { + "description": "Body", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "explanation": { "type": "string", "minLength": 1, "maxLength": 2000 } + } + } + } + } + }, + "responses": { + "200": { + "description": "200", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "suppressionId": { "type": "string" }, + "recipientEmail": { "type": "string", "nullable": true }, + "reason": { + "type": "string", + "enum": [ + "hard_bounce", + "complaint", + "repeated_soft_bounce", + "provider_suppression", + "manual" + ] + }, + "active": { "type": "boolean" }, + "firstSuppressedAt": { "type": "string" }, + "lastSuppressedAt": { "type": "string" }, + "releasedAt": { "type": "string", "nullable": true }, + "releaseReason": { "type": "string", "nullable": true } + }, + "required": [ + "suppressionId", + "recipientEmail", + "reason", + "active", + "firstSuppressedAt", + "lastSuppressedAt" + ] + } + } + } + }, + "404": { + "description": "404", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + }, + "409": { + "description": "409", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + } + } + } + }, + "/billing/action-token": { + "post": { + "summary": "Issue a single-use token for a sensitive billing action", + "tags": ["Billing"], + "parameters": [], + "requestBody": { + "description": "Body", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "action": { + "type": "string", + "enum": [ + "organization_checkout", + "checkout", + "portal", + "plan_change", + "organization_close", + "pending_hide" + ] + }, + "target": { "type": "string", "minLength": 1, "maxLength": 300 } + }, + "required": ["action", "target"] + } + } + } + }, + "responses": { + "201": { + "description": "201", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "token": { "type": "string", "minLength": 32 }, + "expiresAt": { "type": "string", "format": "date-time" } + }, + "required": ["token", "expiresAt"] + } + } + } + }, + "401": { + "description": "401", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + }, + "403": { + "description": "403", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + }, + "503": { + "description": "503", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + } + } + } + }, + "/billing/organization-checkouts": { + "post": { + "summary": "Create a paid organization and hosted checkout", + "tags": ["Billing"], + "parameters": [], + "requestBody": { + "description": "Body", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "plan": { "type": "string", "enum": ["pro", "business"] }, + "interval": { "type": "string", "enum": ["month", "year"] }, + "catalogRevision": { + "type": "integer", + "minimum": 0, + "exclusiveMinimum": 0 + }, + "organizationName": { + "type": "string", + "minLength": 1, + "maxLength": 120 + }, + "teamName": { "type": "string", "minLength": 1, "maxLength": 120 } + }, + "required": [ + "plan", + "interval", + "catalogRevision", + "organizationName", + "teamName" + ] + } + } + } + }, + "responses": { + "201": { + "description": "201", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "checkoutUrl": { "type": "string", "format": "uri" }, + "expiresAt": { "type": "string" }, + "organizationId": { "type": "string" } + }, + "required": ["checkoutUrl", "expiresAt", "organizationId"] + } + } + } + }, + "400": { + "description": "400", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + }, + "401": { + "description": "401", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + }, + "402": { + "description": "402", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + }, + "403": { + "description": "403", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + }, + "409": { + "description": "409", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + }, + "503": { + "description": "503", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + } + } + } + }, + "/billing/catalog": { + "get": { + "description": "Returns environment-configured offers without provider product IDs. Amounts are integer minor units.", + "summary": "Read the active billing catalog", + "tags": ["Billing"], + "parameters": [], + "responses": { + "200": { + "description": "200", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "catalogRevision": { + "type": "integer", + "minimum": 0, + "exclusiveMinimum": 0, + "nullable": true + }, + "currency": { + "type": "string", + "pattern": "^[A-Z]{3}$", + "nullable": true + }, + "offers": { + "type": "array", + "items": { + "type": "object", + "properties": { + "catalogKey": { + "type": "string", + "enum": [ + "pro_month", + "pro_year", + "business_month", + "business_year" + ] + }, + "plan": { "type": "string", "enum": ["pro", "business"] }, + "interval": { "type": "string", "enum": ["month", "year"] }, + "currency": { "type": "string", "pattern": "^[A-Z]{3}$" }, + "amountMinor": { + "type": "integer", + "minimum": 0, + "exclusiveMinimum": 0 + }, + "trialDays": { "type": "integer", "minimum": 0 } + }, + "required": [ + "catalogKey", + "plan", + "interval", + "currency", + "amountMinor", + "trialDays" + ] + } + }, + "checkoutAvailable": { "type": "boolean" } + }, + "required": [ + "catalogRevision", + "currency", + "offers", + "checkoutAvailable" + ] + } + } + } + }, + "503": { + "description": "503", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + } + } + } + }, + "/organizations/{organizationId}/billing": { + "get": { + "summary": "Read organization plan and billing status", + "tags": ["Billing"], + "parameters": [ + { + "name": "organizationId", + "in": "path", + "required": true, + "schema": { "type": "string" } + } + ], + "responses": { + "200": { + "description": "200", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "plan": { + "type": "string", + "enum": ["oss", "free", "pro", "business"] + }, + "billingInterval": { + "type": "string", + "enum": ["month", "year"], + "nullable": true + }, + "paymentStatus": { + "type": "string", + "enum": [ + "free", + "checkout_pending", + "trialing", + "active", + "past_due", + "cancel_at_period_end", + "cancelled", + "expired" + ] + }, + "trialEndsAt": { "type": "string", "nullable": true }, + "currentPeriodEndsAt": { "type": "string", "nullable": true }, + "cancelAtPeriodEnd": { "type": "boolean" }, + "graceEndsAt": { "type": "string", "nullable": true }, + "canManageBilling": { "type": "boolean" }, + "entitlements": { + "type": "object", + "properties": { + "teamsLimit": { + "type": "integer", + "minimum": 0, + "exclusiveMinimum": 0, + "nullable": true + }, + "subscribedContactsLimit": { + "type": "integer", + "minimum": 0, + "exclusiveMinimum": 0, + "nullable": true + }, + "monthlySendsLimit": { + "type": "integer", + "minimum": 0, + "exclusiveMinimum": 0, + "nullable": true + }, + "sharedOrganizationMailbox": { "type": "boolean" }, + "provisioning": { "type": "boolean" }, + "organizationApiKeys": { "type": "boolean" }, + "marketingBranding": { "type": "boolean" } + }, + "required": [ + "teamsLimit", + "subscribedContactsLimit", + "monthlySendsLimit", + "sharedOrganizationMailbox", + "provisioning", + "organizationApiKeys", + "marketingBranding" + ] + }, + "usage": { + "type": "object", + "properties": { + "plan": { + "type": "string", + "enum": ["oss", "free", "pro", "business"] + }, + "paymentStatus": { + "type": "string", + "enum": [ + "free", + "checkout_pending", + "trialing", + "active", + "past_due", + "cancel_at_period_end", + "cancelled", + "expired" + ] + }, + "teams": { "type": "integer", "minimum": 0 }, + "subscribedContacts": { "type": "integer", "minimum": 0 }, + "monthlySends": { "type": "integer", "minimum": 0 }, + "monthlySendsReserved": { "type": "integer", "minimum": 0 }, + "bucketStartsAt": { "type": "string" }, + "bucketEndsAt": { "type": "string" }, + "teamsLimit": { + "type": "integer", + "minimum": 0, + "exclusiveMinimum": 0, + "nullable": true + }, + "subscribedContactsLimit": { + "type": "integer", + "minimum": 0, + "exclusiveMinimum": 0, + "nullable": true + }, + "monthlySendsLimit": { + "type": "integer", + "minimum": 0, + "exclusiveMinimum": 0, + "nullable": true + } + }, + "required": [ + "plan", + "paymentStatus", + "teams", + "subscribedContacts", + "monthlySends", + "monthlySendsReserved", + "bucketStartsAt", + "bucketEndsAt", + "teamsLimit", + "subscribedContactsLimit", + "monthlySendsLimit" + ] + }, + "pendingPlanChange": { + "type": "object", + "properties": { + "changeId": { "type": "string" }, + "targetPlan": { "type": "string", "enum": ["pro", "business"] }, + "targetInterval": { + "type": "string", + "enum": ["month", "year"] + }, + "effectiveAt": { + "type": "string", + "enum": ["immediately", "next_billing_date"] + } + }, + "required": [ + "changeId", + "targetPlan", + "targetInterval", + "effectiveAt" + ], + "nullable": true + } + }, + "required": [ + "plan", + "billingInterval", + "paymentStatus", + "trialEndsAt", + "currentPeriodEndsAt", + "cancelAtPeriodEnd", + "graceEndsAt", + "canManageBilling", + "entitlements", + "usage", + "pendingPlanChange" + ] + } + } + } + }, + "403": { + "description": "403", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + }, + "404": { + "description": "404", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + } + } + } + }, + "/organizations/{organizationId}/plan-usage": { + "get": { + "summary": "Read organization plan usage", + "tags": ["Billing"], + "parameters": [ + { + "name": "organizationId", + "in": "path", + "required": true, + "schema": { "type": "string" } + } + ], + "responses": { + "200": { + "description": "200", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "plan": { + "type": "string", + "enum": ["oss", "free", "pro", "business"] + }, + "paymentStatus": { + "type": "string", + "enum": [ + "free", + "checkout_pending", + "trialing", + "active", + "past_due", + "cancel_at_period_end", + "cancelled", + "expired" + ] + }, + "teams": { "type": "integer", "minimum": 0 }, + "subscribedContacts": { "type": "integer", "minimum": 0 }, + "monthlySends": { "type": "integer", "minimum": 0 }, + "monthlySendsReserved": { "type": "integer", "minimum": 0 }, + "bucketStartsAt": { "type": "string" }, + "bucketEndsAt": { "type": "string" }, + "teamsLimit": { + "type": "integer", + "minimum": 0, + "exclusiveMinimum": 0, + "nullable": true + }, + "subscribedContactsLimit": { + "type": "integer", + "minimum": 0, + "exclusiveMinimum": 0, + "nullable": true + }, + "monthlySendsLimit": { + "type": "integer", + "minimum": 0, + "exclusiveMinimum": 0, + "nullable": true + } + }, + "required": [ + "plan", + "paymentStatus", + "teams", + "subscribedContacts", + "monthlySends", + "monthlySendsReserved", + "bucketStartsAt", + "bucketEndsAt", + "teamsLimit", + "subscribedContactsLimit", + "monthlySendsLimit" + ] + } + } + } + }, + "403": { + "description": "403", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + }, + "404": { + "description": "404", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + } + } + } + }, + "/organizations/{organizationId}/billing/checkout": { + "post": { + "summary": "Create a hosted organization checkout", + "tags": ["Billing"], + "parameters": [ + { + "name": "organizationId", + "in": "path", + "required": true, + "schema": { "type": "string" } + } + ], + "requestBody": { + "description": "Body", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "plan": { "type": "string", "enum": ["pro", "business"] }, + "interval": { "type": "string", "enum": ["month", "year"] }, + "catalogRevision": { + "type": "integer", + "minimum": 0, + "exclusiveMinimum": 0 + } + }, + "required": ["plan", "interval", "catalogRevision"] + } + } + } + }, + "responses": { + "201": { + "description": "201", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "checkoutUrl": { "type": "string", "format": "uri" }, + "expiresAt": { "type": "string" } + }, + "required": ["checkoutUrl", "expiresAt"] + } + } + } + }, + "400": { + "description": "400", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + }, + "401": { + "description": "401", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + }, + "402": { + "description": "402", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + }, + "403": { + "description": "403", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + }, + "409": { + "description": "409", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + }, + "503": { + "description": "503", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + } + } + } + }, + "/organizations/{organizationId}/billing/portal": { + "post": { + "summary": "Create the billing manager's hosted portal session", + "tags": ["Billing"], + "parameters": [ + { + "name": "organizationId", + "in": "path", + "required": true, + "schema": { "type": "string" } + } + ], + "responses": { + "201": { + "description": "201", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "portalUrl": { "type": "string", "format": "uri" } }, + "required": ["portalUrl"] + } + } + } + }, + "401": { + "description": "401", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + }, + "402": { + "description": "402", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + }, + "403": { + "description": "403", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + }, + "404": { + "description": "404", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + }, + "503": { + "description": "503", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + } + } + } + }, + "/organizations/{organizationId}/billing/plan-change": { + "post": { + "summary": "Request a SendLit-owned organization plan change", + "tags": ["Billing"], + "parameters": [ + { + "name": "organizationId", + "in": "path", + "required": true, + "schema": { "type": "string" } + } + ], + "requestBody": { + "description": "Body", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "plan": { "type": "string", "enum": ["pro", "business"] }, + "interval": { "type": "string", "enum": ["month", "year"] }, + "catalogRevision": { + "type": "integer", + "minimum": 0, + "exclusiveMinimum": 0 + }, + "idempotencyKey": { + "type": "string", + "minLength": 1, + "maxLength": 256 + } + }, + "required": ["plan", "interval", "catalogRevision"] + } + } + } + }, + "responses": { + "200": { + "description": "200", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "changeId": { "type": "string" }, + "status": { + "type": "string", + "enum": ["pending", "succeeded", "failed", "conflicted"] + }, + "targetPlan": { "type": "string", "enum": ["pro", "business"] }, + "targetInterval": { "type": "string", "enum": ["month", "year"] }, + "effectiveAt": { + "type": "string", + "enum": ["immediately", "next_billing_date"] + }, + "paymentUrl": { + "type": "string", + "format": "uri", + "nullable": true + }, + "completedAt": { "type": "string", "nullable": true } + }, + "required": [ + "changeId", + "status", + "targetPlan", + "targetInterval", + "effectiveAt", + "paymentUrl", + "completedAt" + ] + } + } + } + }, + "202": { + "description": "202", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "changeId": { "type": "string" }, + "status": { + "type": "string", + "enum": ["pending", "succeeded", "failed", "conflicted"] + }, + "targetPlan": { "type": "string", "enum": ["pro", "business"] }, + "targetInterval": { "type": "string", "enum": ["month", "year"] }, + "effectiveAt": { + "type": "string", + "enum": ["immediately", "next_billing_date"] + }, + "paymentUrl": { + "type": "string", + "format": "uri", + "nullable": true + }, + "completedAt": { "type": "string", "nullable": true } + }, + "required": [ + "changeId", + "status", + "targetPlan", + "targetInterval", + "effectiveAt", + "paymentUrl", + "completedAt" + ] + } + } + } + }, + "400": { + "description": "400", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + }, + "401": { + "description": "401", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + }, + "402": { + "description": "402", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + }, + "403": { + "description": "403", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + }, + "404": { + "description": "404", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + }, + "409": { + "description": "409", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + }, + "503": { + "description": "503", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + } + } + } + }, + "/organizations/{organizationId}/billing/plan-changes/{changeId}": { + "get": { + "summary": "Read a plan-change request status", + "tags": ["Billing"], + "parameters": [ + { + "name": "organizationId", + "in": "path", + "required": true, + "schema": { "type": "string" } + }, + { + "name": "changeId", + "in": "path", + "required": true, + "schema": { "type": "string" } + } + ], + "responses": { + "200": { + "description": "200", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "changeId": { "type": "string" }, + "status": { + "type": "string", + "enum": ["pending", "succeeded", "failed", "conflicted"] + }, + "targetPlan": { "type": "string", "enum": ["pro", "business"] }, + "targetInterval": { "type": "string", "enum": ["month", "year"] }, + "effectiveAt": { + "type": "string", + "enum": ["immediately", "next_billing_date"] + }, + "paymentUrl": { + "type": "string", + "format": "uri", + "nullable": true + }, + "completedAt": { "type": "string", "nullable": true } + }, + "required": [ + "changeId", + "status", + "targetPlan", + "targetInterval", + "effectiveAt", + "paymentUrl", + "completedAt" + ] + } + } + } + }, + "403": { + "description": "403", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + }, + "404": { + "description": "404", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "error": { "type": "string" } }, + "required": ["error"] + } + } + } + } + } + } + } + }, + "info": { + "title": "SendLit API", + "description": "SendLit REST API for composing, sending and automating email. Authenticate with a team-scoped API key or a Better Auth OAuth2 bearer token.", + "version": "0.1.0" + }, + "servers": [ + { + "url": "{protocol}://{host}", + "description": "API Server", + "variables": { + "protocol": { "default": "https", "enum": ["https", "http"] }, + "host": { "default": "api.sendlit.app" } + } + } + ], + "tags": [ + { "name": "Contacts", "description": "Manage contacts (subscribers)." }, + { + "name": "Segments", + "description": "Saved, named, reusable contact filters - build a filter once, reuse it across broadcasts and sequences." + }, + { + "name": "Media", + "description": "Authenticated MediaLit upload signatures for dashboard-owned media." + }, + { "name": "Templates", "description": "Reusable email templates." }, + { + "name": "Sequences", + "description": "Broadcasts (one-off) and sequences (multi-step, event-triggered)." + }, + { + "name": "Transactional Emails", + "description": "Single API-triggered sends (receipts, password resets, ...) — no audience filter, no unsubscribe footer, delivered immediately." + }, + { + "name": "Settings", + "description": "Per-team settings, including email sending provider (SMTP) configuration and test sends." + }, + { + "name": "Teams", + "description": "Team management (list/create/rename/delete), per-team API keys, and server-to-server provisioning." + }, + { + "name": "Delivery", + "description": "Normalized bounce/complaint delivery events and the per-workspace suppression (do-not-send) list. See docs/bounces-and-complaints.md." + }, + { + "name": "Billing", + "description": "Organization-scoped plan, configured catalog, entitlement, and usage information." + } + ], + "components": { + "securitySchemes": { + "apiKeyAuth": { "type": "apiKey", "in": "header", "name": "x-sendlit-apikey" }, + "bearerAuth": { "type": "http", "scheme": "bearer", "bearerFormat": "JWT" } + } + }, + "security": [{ "apiKeyAuth": [] }, { "bearerAuth": [] }] +} diff --git a/apps/api/src/documentation/openapi/import.test.ts b/apps/api/src/documentation/openapi/import.test.ts new file mode 100644 index 0000000..047811c --- /dev/null +++ b/apps/api/src/documentation/openapi/import.test.ts @@ -0,0 +1,192 @@ +import { readFileSync } from "node:fs"; +import { join } from "node:path"; +import { describe, expect, it } from "vitest"; +import { openApiDocument } from "../../openapi"; +import { importOpenApiSpec } from "./import"; +import { OpenApiImportError } from "./parse"; + +const fixture = (name: string) => + readFileSync(join(__dirname, "fixtures", name), "utf8"); + +function errorsOf(text: string, file = "openapi.yaml") { + try { + importOpenApiSpec({ text, file, mount: "api-reference" }); + } catch (error) { + if (error instanceof OpenApiImportError) return error.diagnostics; + throw error; + } + return []; +} + +describe("importOpenApiSpec", () => { + it("imports MediaLit's committed spec", () => { + const result = importOpenApiSpec({ + text: fixture("medialit.json"), + file: "apps/api/src/swagger_output.json", + mount: "api", + }); + expect(result.info.title).toBeTruthy(); + expect(result.operations).toHaveLength(11); + const routes = result.operations.map((o) => o.route); + expect(new Set(routes).size).toBe(routes.length); + expect(routes.every((r) => r.startsWith("api/"))).toBe(true); + + const upload = result.operations.find( + (o) => o.slice.requestBody?.content[0]?.contentType === "multipart/form-data", + )!; + expect(upload.slice.playground.body?.kind).toBe("multipart"); + expect(upload.slice.playground.body?.fields.some((f) => f.binary)).toBe(true); + expect(upload.slice.codeSamples.map((s) => s.label)).toEqual([ + "cURL", + "JavaScript", + "Python", + ]); + expect(upload.slice.codeSamples[0].source).toContain("https://api.medialit.cloud"); + + // Public endpoints (`security: []`) need no auth; signatureAuth is a header key. + expect(result.operations.some((o) => o.slice.playground.auth.length === 0)).toBe( + true, + ); + const withSignature = result.operations.find((o) => + o.slice.playground.auth.some((alt) => alt.some((s) => s.key === "signatureAuth")), + )!; + expect( + withSignature.slice.playground.auth.flat().find((s) => s.key === "signatureAuth"), + ).toMatchObject({ kind: "apiKey", in: "header", name: "x-medialit-signature" }); + expect(result.operations.at(-1)?.group).toBe("Other"); + }); + + it("imports SendLit's spec with readable routes from summaries", () => { + const result = importOpenApiSpec({ + text: fixture("sendlit.json"), + file: "openapi.json", + mount: "api-reference", + }); + expect(result.operations).toHaveLength(137); + expect(result.operations.map((o) => o.route)).toContain( + "api-reference/remove-a-tag-from-a-contact", + ); + const op = result.operations.find( + (o) => o.route === "api-reference/remove-a-tag-from-a-contact", + )!; + expect(op.method).toBe("delete"); + expect(op.slice.playground.servers[0].url).toBe("{protocol}://{host}"); + expect(op.slice.playground.auth).toEqual([ + [{ kind: "apiKey", key: "apiKeyAuth", in: "header", name: "x-sendlit-apikey" }], + [{ kind: "bearer", key: "bearerAuth" }], + ]); + expect(op.slice.codeSamples[0].source).toContain( + "https://api.sendlit.app/contacts/{contactId}/tags/{tag}", + ); + expect(op.chunks[0].content).toMatch( + /^DELETE \/contacts\/\{contactId\}\/tags\/\{tag\}/, + ); + // Groups follow the spec's tag order. + expect(result.operations[0].group).toBe("Contacts"); + expect( + result.warnings.filter((w) => w.code !== "OPENAPI_DESCRIPTION_TEXT"), + ).toEqual([]); + }); + + it("imports FrontLit's own generated spec", () => { + const result = importOpenApiSpec({ + text: JSON.stringify(openApiDocument), + file: "openapi.json", + mount: "api-reference", + }); + expect(result.operations.length).toBeGreaterThan(20); + }); + + it("keeps property order and serves the spec without hidden operations", () => { + const text = `openapi: 3.1.0 +info: { title: Demo, version: "1" } +servers: [{ url: https://api.example.com }] +paths: + /things: + post: + summary: Create a thing + requestBody: + content: + application/json: + schema: { $ref: "#/components/schemas/Thing" } + responses: + "201": { description: Created } + get: + summary: List things + x-internal: true + responses: { "200": { description: OK } } +components: + schemas: + Thing: + type: object + properties: + zeta: { type: string } + alpha: { type: integer } + child: { $ref: "#/components/schemas/Thing" } +`; + const result = importOpenApiSpec({ text, file: "openapi.yaml", mount: "api" }); + expect(result.operations.map((o) => o.title)).toEqual(["Create a thing"]); + const schema = result.operations[0].slice.schemas["#/components/schemas/Thing"]; + expect(Object.keys(schema.properties as object)).toEqual([ + "zeta", + "alpha", + "child", + ]); + expect(result.operations[0].slice.requestBody?.content[0].example).toContain( + '"zeta"', + ); + const served = JSON.parse(result.spec); + expect(served.paths["/things"].get).toBeUndefined(); + expect(served.paths["/things"].post).toBeDefined(); + }); + + it("reports blocking problems with locations", () => { + expect(errorsOf(`swagger: "2.0"\ninfo: { title: Old }\n`)[0]).toMatchObject({ + code: "OPENAPI_UNSUPPORTED", + fieldPath: "/swagger", + line: 1, + }); + const external = errorsOf(`openapi: 3.0.3 +info: { title: Demo, version: "1" } +paths: + /a: + get: + responses: + "200": + $ref: "./responses.yaml#/ok" +`); + expect(external[0]).toMatchObject({ + code: "OPENAPI_UNSUPPORTED", + fieldPath: "/paths/~1a/get/responses/200/$ref", + line: 8, + }); + const duplicates = errorsOf(`openapi: 3.0.3 +info: { title: Demo, version: "1" } +paths: + /a: + get: { operationId: same, responses: {} } + /b: + get: { operationId: same, responses: {} } +`); + expect(duplicates.map((d) => d.code)).toContain("OPENAPI_DUPLICATE_OPERATION"); + expect( + errorsOf(`openapi: 3.0.3\ninfo: { title: x }\na: &a [1]\nb: *a\n`)[0].code, + ).toBe("OPENAPI_PARSE"); + }); + + it("warns when there is no absolute server and disables Try it", () => { + const result = importOpenApiSpec({ + text: `openapi: 3.0.3 +info: { title: Demo, version: "1" } +servers: [{ url: /v1 }] +paths: + /a: { get: { summary: A, responses: { "200": { description: OK } } } } +`, + file: "openapi.yaml", + mount: "api", + }); + expect(result.warnings.map((w) => w.code)).toContain("OPENAPI_NO_SERVER"); + expect(result.operations[0].slice.playground.unavailable).toBeTruthy(); + expect(result.operations[0].slice.codeSamples[0].source).toContain("{baseUrl}/a"); + }); +}); diff --git a/apps/api/src/documentation/openapi/import.ts b/apps/api/src/documentation/openapi/import.ts new file mode 100644 index 0000000..8cae030 --- /dev/null +++ b/apps/api/src/documentation/openapi/import.ts @@ -0,0 +1,698 @@ +import { createHash } from "node:crypto"; +import { + diagnostic, + parseDocsMarkdown, + type DocsDiagnostic, +} from "@frontlit/text-editor/docs"; +import type { + ApiCodeSample, + ApiMediaContent, + ApiOperationSlice, + ApiParameter, + ApiParameterLocation, + ApiPlaygroundAuth, + ApiPlaygroundField, + ApiPlaygroundModel, + ApiReferenceInfo, + ApiResponse, + ApiRichText, + ApiSecurityScheme, + ApiServer, +} from "@frontlit/api-contract"; +import { + locateDiagnostics, + OPENAPI_LIMITS, + OpenApiImportError, + parseSpecText, + pointer, +} from "./parse"; +import { + collectSchemaRefs, + deref, + enumValues, + isBinarySchema, + mediaExample, + objectProperties, + resolvePointer, +} from "./schema"; +import { generateCodeSamples, resolveServerUrl } from "./samples"; + +type Json = any; +const METHODS = ["get", "put", "post", "delete", "options", "head", "patch", "trace"]; + +export interface ApiChunk { + heading: string | null; + anchor: string | null; + content: string; +} +export interface ImportedOperation { + route: string; + title: string; + group: string; + order: number; + method: string; + path: string; + deprecated: boolean; + slice: ApiOperationSlice; + chunks: ApiChunk[]; +} +export interface ImportedSpec { + info: ApiReferenceInfo; + operations: ImportedOperation[]; + /** The served `/openapi.json`: the committed spec as JSON, hidden operations removed. */ + spec: string; + specHash: string; + warnings: DocsDiagnostic[]; +} + +export function slugify(value: string) { + return value + .replace(/([a-z0-9])([A-Z])/g, "$1-$2") + .toLowerCase() + .replace(/[^a-z0-9]+/g, "-") + .replace(/^-+|-+$/g, "") + .slice(0, 80) + .replace(/-+$/g, ""); +} + +export function normalizeApiMount(value: string | undefined | null) { + return slugify(value ?? "") || "api-reference"; +} + +const isHidden = (operation: Json) => + operation?.["x-internal"] === true || operation?.["x-excluded"] === true; + +const isObject = (value: unknown): value is Record => + !!value && typeof value === "object" && !Array.isArray(value); + +const text = (value: unknown) => (typeof value === "string" ? value.trim() : ""); + +function richText( + markdown: unknown, + file: string, + warnings: DocsDiagnostic[], + at: string, +) { + const source = text(markdown); + if (!source) return undefined; + try { + const { content } = parseDocsMarkdown(source, { file, frontmatter: false }); + return { kind: "doc", content } as ApiRichText; + } catch { + warnings.push({ + ...diagnostic( + "OPENAPI_DESCRIPTION_TEXT", + "This description uses Markdown or HTML that docs don't support, so it is shown as plain text.", + { file, fieldPath: at }, + ), + severity: "warning", + blocksSave: false, + blocksPublish: false, + }); + return { kind: "text", text: source } as ApiRichText; + } +} + +function plainText(rich: ApiRichText | undefined): string { + if (!rich) return ""; + if (rich.kind === "text") return rich.text; + const parts: string[] = []; + const walk = (node: Json) => { + if (typeof node?.text === "string") parts.push(node.text); + for (const child of node?.content ?? []) walk(child); + if (["paragraph", "heading", "listItem", "codeBlock"].includes(node?.type)) + parts.push("\n"); + }; + walk(rich.content); + return parts + .join("") + .replace(/\n{2,}/g, "\n") + .trim(); +} + +function safeUrl(value: unknown) { + return typeof value === "string" && /^(https?:|mailto:|\/|\{)/i.test(value.trim()) + ? value.trim() + : undefined; +} + +function servers(list: unknown): ApiServer[] { + if (!Array.isArray(list)) return []; + return list.flatMap((server: Json) => { + const url = safeUrl(server?.url); + if (!url) return []; + return [ + { + url, + description: text(server.description) || undefined, + variables: Object.entries( + isObject(server.variables) ? server.variables : {}, + ).map(([name, variable]) => ({ + name, + default: String(variable?.default ?? ""), + enum: Array.isArray(variable?.enum) ? variable.enum.map(String) : undefined, + description: text(variable?.description) || undefined, + })), + }, + ]; + }); +} + +function securitySchemes(root: Json): ApiSecurityScheme[] { + return Object.entries(root.components?.securitySchemes ?? {}).flatMap( + ([key, raw]) => { + const scheme = deref(root, raw); + if (!isObject(scheme) || typeof scheme.type !== "string") return []; + return [ + { + key, + type: scheme.type, + in: scheme.in, + name: typeof scheme.name === "string" ? scheme.name : undefined, + scheme: + typeof scheme.scheme === "string" ? scheme.scheme.toLowerCase() : undefined, + bearerFormat: text(scheme.bearerFormat) || undefined, + description: text(scheme.description) || undefined, + } as ApiSecurityScheme, + ]; + }, + ); +} + +function playgroundAuth( + scheme: ApiSecurityScheme | undefined, + key: string, +): ApiPlaygroundAuth { + if (!scheme) return { kind: "unsupported", key, reason: "Unknown security scheme." }; + if (scheme.type === "apiKey") { + if (scheme.in === "header" || scheme.in === "query") + return { kind: "apiKey", key, in: scheme.in, name: scheme.name ?? key }; + return { + kind: "unsupported", + key, + reason: + "Browsers can't set cookies for another site, so this key can't be sent from here.", + }; + } + if (scheme.type === "http" && scheme.scheme === "bearer") + return { kind: "bearer", key }; + if (scheme.type === "http" && scheme.scheme === "basic") + return { kind: "basic", key }; + if (scheme.type === "oauth2" || scheme.type === "openIdConnect") + return { kind: "bearer", key }; + return { + kind: "unsupported", + key, + reason: "This authentication type isn't supported here.", + }; +} + +const prettyExample = (value: unknown, contentType: string) => { + if (value === undefined) return undefined; + if (typeof value === "string" && !/json/i.test(contentType)) return value; + try { + return JSON.stringify(value, null, 2); + } catch { + return undefined; + } +}; + +function mediaContents(root: Json, content: Json, direction: "request" | "response") { + return Object.entries(isObject(content) ? content : {}).map( + ([contentType, media]): ApiMediaContent => ({ + contentType, + schema: isObject(media?.schema) ? media.schema : undefined, + example: prettyExample(mediaExample(root, media, direction), contentType), + }), + ); +} + +function bodyKind(mediaType: string): "json" | "multipart" | "form" | "text" { + if (/json/i.test(mediaType)) return "json"; + if (/multipart\/form-data/i.test(mediaType)) return "multipart"; + if (/x-www-form-urlencoded/i.test(mediaType)) return "form"; + return "text"; +} + +const exampleString = (value: unknown) => + value === undefined || value === null + ? undefined + : typeof value === "string" + ? value + : JSON.stringify(value); + +function chunksFor(operation: ApiOperationSlice, root: Json): ApiChunk[] { + const header = `${operation.method.toUpperCase()} ${operation.path} — ${operation.title} (${operation.group})`; + const schemaLines = (schema: Json) => { + const { properties, required } = objectProperties(root, schema); + return Object.entries(properties).map(([name, property]) => { + const resolved = property?.$ref ? resolvePointer(root, property.$ref) : property; + const type = Array.isArray(resolved?.type) + ? resolved.type.join(" | ") + : (resolved?.type ?? (resolved?.$ref ? "object" : "any")); + const flags = required.includes(name) ? ", required" : ""; + const description = text(resolved?.description); + return `${name} (${type}${flags})${description ? `: ${description}` : ""}`; + }); + }; + const auth = operation.security.length + ? `Authentication: ${operation.security + .map((requirement) => + requirement + .map((key) => { + const scheme = operation.securitySchemes.find((s) => s.key === key); + if (!scheme) return key; + if (scheme.type === "apiKey") + return `API key in ${scheme.in} "${scheme.name}"`; + if (scheme.type === "http") return `HTTP ${scheme.scheme}`; + return scheme.type; + }) + .join(" and "), + ) + .join(" or ")}.` + : "Authentication: none."; + const chunks: ApiChunk[] = [ + { + heading: operation.title, + anchor: null, + content: [header, plainText(operation.description), auth] + .filter(Boolean) + .join("\n"), + }, + ]; + if (operation.parameters.length) + chunks.push({ + heading: "Parameters", + anchor: "parameters", + content: [ + header, + ...operation.parameters.map( + (p) => + `${p.name} (${p.in}${p.required ? ", required" : ""})${p.description ? `: ${p.description}` : ""}`, + ), + ].join("\n"), + }); + if (operation.requestBody) + chunks.push({ + heading: "Request body", + anchor: "request-body", + content: [ + header, + operation.requestBody.description ?? "", + ...operation.requestBody.content.flatMap((c) => [ + `Content type: ${c.contentType}`, + ...schemaLines(c.schema), + ]), + ] + .filter(Boolean) + .join("\n"), + }); + for (const response of operation.responses) + chunks.push({ + heading: `Response ${response.status}`, + anchor: `response-${response.status.toLowerCase()}`, + content: [ + header, + `Response ${response.status}${response.description ? `: ${response.description}` : ""}`, + ...response.content.flatMap((c) => schemaLines(c.schema)), + ].join("\n"), + }); + return chunks; +} + +/** Walks every `$ref` with its JSON pointer. */ +function eachRef(value: Json, visit: (ref: string, at: (string | number)[]) => void) { + const walk = (node: Json, at: (string | number)[]) => { + if (Array.isArray(node)) return node.forEach((child, i) => walk(child, [...at, i])); + if (!isObject(node)) return; + if (typeof node.$ref === "string") visit(node.$ref, [...at, "$ref"]); + for (const [key, child] of Object.entries(node)) walk(child, [...at, key]); + }; + walk(value, []); +} + +/** Imports one single-file OpenAPI 3.0/3.1 spec into operation pages. Throws + * `OpenApiImportError` with every blocking problem found. */ +export function importOpenApiSpec(input: { + text: string; + file: string; + mount: string; +}): ImportedSpec { + const { file } = input; + const mount = normalizeApiMount(input.mount); + const fail = (items: DocsDiagnostic[]): never => { + throw new OpenApiImportError(locateDiagnostics(input.text, items)); + }; + if (Buffer.byteLength(input.text) > OPENAPI_LIMITS.bytes) + fail([diagnostic("OPENAPI_LIMIT", "The spec is larger than 5 MiB.", { file })]); + const root = parseSpecText(input.text, file) as Json; + if (!isObject(root)) + fail([ + diagnostic("OPENAPI_INVALID", "The spec must be a JSON or YAML object.", { + file, + }), + ]); + if (typeof root.swagger === "string") + fail([ + diagnostic( + "OPENAPI_UNSUPPORTED", + "OpenAPI 2.0 (Swagger) isn't supported. Convert the spec to OpenAPI 3.0 or 3.1.", + { file, fieldPath: "/swagger" }, + ), + ]); + const errors: DocsDiagnostic[] = []; + const warnings: DocsDiagnostic[] = []; + if (typeof root.openapi !== "string" || !/^3\.[01]\./.test(root.openapi)) + errors.push( + diagnostic( + "OPENAPI_UNSUPPORTED", + "Only OpenAPI 3.0 and 3.1 specs are supported (the `openapi` field).", + { file, fieldPath: "/openapi" }, + ), + ); + if (!text(root.info?.title)) + errors.push( + diagnostic("OPENAPI_INVALID", "`info.title` is required.", { + file, + fieldPath: "/info/title", + }), + ); + if (root.paths !== undefined && !isObject(root.paths)) + errors.push( + diagnostic("OPENAPI_INVALID", "`paths` must be an object.", { + file, + fieldPath: "/paths", + }), + ); + eachRef(root, (ref, at) => { + if (!ref.startsWith("#")) + errors.push( + diagnostic( + "OPENAPI_UNSUPPORTED", + "References to other files aren't supported. Bundle the spec into a single file.", + { file, fieldPath: pointer(at) }, + ), + ); + else if (resolvePointer(root, ref) === undefined) + errors.push( + diagnostic( + "OPENAPI_INVALID", + `\`${ref}\` doesn't point to anything in the spec.`, + { + file, + fieldPath: pointer(at), + }, + ), + ); + }); + if (errors.length) fail(errors); + + const schemes = securitySchemes(root); + const rootServers = servers(root.servers); + const tagOrder = (Array.isArray(root.tags) ? root.tags : []) + .map((tag: Json) => text(tag?.name)) + .filter(Boolean) as string[]; + const served = structuredClone(root); + const operations: ImportedOperation[] = []; + const operationIds = new Map(); + const routes = new Map(); + + for (const [path, rawItem] of Object.entries( + isObject(root.paths) ? root.paths : {}, + )) { + const item = deref(root, rawItem); + if (!isObject(item)) continue; + for (const method of METHODS) { + const operation = item[method]; + if (!isObject(operation)) continue; + const at = ["paths", path, method]; + if (isHidden(operation)) { + delete served.paths?.[path]?.[method]; + continue; + } + const operationId = text(operation.operationId) || undefined; + if (operationId) { + const previous = operationIds.get(operationId); + if (previous) + errors.push( + diagnostic( + "OPENAPI_DUPLICATE_OPERATION", + `\`operationId\` "${operationId}" is used twice (also at ${previous}).`, + { file, fieldPath: pointer([...at, "operationId"]) }, + ), + ); + operationIds.set(operationId, pointer(at)); + } + const summary = text(operation.summary); + const title = summary || `${method.toUpperCase()} ${path}`; + const slug = + slugify(operationId ?? "") || slugify(summary) || slugify(`${method} ${path}`); + const route = `${mount}/${slug}`; + const clash = routes.get(route); + if (clash) + errors.push( + diagnostic( + "ROUTE_COLLISION", + `Two operations would be published at /docs/${route} (also ${clash}). Give one a distinct \`operationId\` or summary.`, + { file, fieldPath: pointer(at), route: `/docs/${route}` }, + ), + ); + routes.set(route, pointer(at)); + const group = + text(Array.isArray(operation.tags) ? operation.tags[0] : "") || "Other"; + + const merged = new Map(); + for (const list of [item.parameters, operation.parameters]) + for (const raw of Array.isArray(list) ? list : []) { + const parameter = deref(root, raw); + if (isObject(parameter) && typeof parameter.name === "string") + merged.set(`${parameter.in}:${parameter.name}`, parameter); + } + const parameters: ApiParameter[] = [...merged.values()].map((p) => ({ + name: p.name, + in: p.in as ApiParameterLocation, + required: p.in === "path" ? true : p.required === true, + deprecated: p.deprecated === true, + description: text(p.description) || undefined, + schema: isObject(p.schema) + ? p.schema + : isObject(p.content) + ? (Object.values(p.content)[0]?.schema ?? undefined) + : undefined, + })); + const requestBodyRaw = deref(root, operation.requestBody); + const requestBody = isObject(requestBodyRaw) + ? { + required: requestBodyRaw.required === true, + description: text(requestBodyRaw.description) || undefined, + content: mediaContents(root, requestBodyRaw.content, "request"), + } + : undefined; + const responses: ApiResponse[] = Object.entries( + isObject(operation.responses) ? operation.responses : {}, + ).map(([status, raw]) => { + const response = deref(root, raw); + return { + status, + description: text(response?.description) || undefined, + content: mediaContents(root, response?.content, "response"), + }; + }); + const security = ( + Array.isArray(operation.security) + ? operation.security + : Array.isArray(root.security) + ? root.security + : [] + ) + .filter(isObject) + .map((requirement: Json) => Object.keys(requirement)); + // `{}` inside the list means "or no auth at all". + const optionalAuth = security.some( + (requirement: string[]) => !requirement.length, + ); + const requirements = optionalAuth ? [] : security; + const usedSchemes = schemes.filter((s) => + requirements.some((requirement: string[]) => requirement.includes(s.key)), + ); + const operationServers = servers(operation.servers).length + ? servers(operation.servers) + : servers(item.servers).length + ? servers(item.servers) + : rootServers; + const schemas = Object.fromEntries( + collectSchemaRefs(root, [parameters, requestBody, responses]), + ); + + const firstBody = requestBody?.content[0]; + const bodyFields: ApiPlaygroundField[] = []; + if (firstBody && bodyKind(firstBody.contentType) !== "json") { + const { properties, required } = objectProperties(root, firstBody.schema); + for (const [name, property] of Object.entries(properties)) { + const resolved = property?.$ref + ? resolvePointer(root, property.$ref) + : property; + bodyFields.push({ + name, + required: required.includes(name), + binary: isBinarySchema(root, property), + description: text(resolved?.description) || undefined, + enum: enumValues(root, property), + example: exampleString(resolved?.example ?? resolved?.default), + }); + } + } + const absolute = operationServers.filter((s) => + /^https?:\/\//i.test(resolveServerUrl(s.url, s.variables)), + ); + const playground: ApiPlaygroundModel = { + method, + path, + servers: absolute, + unavailable: absolute.length + ? undefined + : "This API's spec has no absolute server URL, so requests can't be sent from the docs.", + parameters: parameters + .filter((p) => p.in !== "cookie") + .map((p) => { + const resolved = p.schema?.$ref + ? resolvePointer(root, p.schema.$ref as string) + : p.schema; + return { + name: p.name, + in: p.in, + required: p.required, + binary: false, + description: p.description, + enum: enumValues(root, p.schema), + example: exampleString( + merged.get(`${p.in}:${p.name}`)?.example ?? + resolved?.example ?? + resolved?.default, + ), + }; + }), + body: firstBody + ? { + mediaType: firstBody.contentType, + kind: bodyKind(firstBody.contentType), + required: requestBody!.required, + example: firstBody.example, + fields: bodyFields, + } + : undefined, + auth: requirements.map((requirement: string[]) => + requirement.map((key) => + playgroundAuth( + schemes.find((s) => s.key === key), + key, + ), + ), + ), + }; + const customSamples: ApiCodeSample[] = ( + operation["x-codeSamples"] ?? + operation["x-code-samples"] ?? + [] + ) + .filter((s: Json) => isObject(s) && typeof s.source === "string") + .map((s: Json) => ({ + label: text(s.label) || text(s.lang) || "Example", + lang: text(s.lang).toLowerCase() || "text", + source: s.source, + })); + const slice: ApiOperationSlice = { + method, + path, + operationId, + title, + description: richText( + operation.description, + file, + warnings, + pointer([...at, "description"]), + ), + group, + deprecated: operation.deprecated === true, + parameters, + requestBody, + responses, + security: requirements, + securitySchemes: usedSchemes, + servers: operationServers, + schemas, + codeSamples: [...generateCodeSamples(playground), ...customSamples], + playground, + }; + operations.push({ + route, + title, + group, + order: operations.length, + method, + path, + deprecated: slice.deprecated, + slice, + chunks: chunksFor(slice, root), + }); + } + } + if (operations.length > OPENAPI_LIMITS.operations) + errors.push( + diagnostic( + "OPENAPI_LIMIT", + `The spec has ${operations.length} operations; at most ${OPENAPI_LIMITS.operations} are supported.`, + { file }, + ), + ); + if (errors.length) fail(errors); + if (!operations.length) + warnings.push({ + ...diagnostic("OPENAPI_EMPTY", "The spec has no public operations to publish.", { + file, + }), + severity: "warning", + blocksSave: false, + blocksPublish: false, + }); + if ( + !rootServers.some((s) => /^https?:\/\//i.test(resolveServerUrl(s.url, s.variables))) + ) + warnings.push({ + ...diagnostic( + "OPENAPI_NO_SERVER", + "The spec has no absolute server URL (like https://api.example.com), so “Try it” is turned off and code samples use {baseUrl}.", + { file, fieldPath: "/servers" }, + ), + severity: "warning", + blocksSave: false, + blocksPublish: false, + }); + + // Groups follow the spec's `tags` order, then first appearance; "Other" last. + const groupRank = (group: string) => { + const index = tagOrder.indexOf(group); + if (index !== -1) return index; + if (group === "Other") return Number.MAX_SAFE_INTEGER; + return tagOrder.length + operations.findIndex((o) => o.group === group); + }; + operations.sort( + (a, b) => groupRank(a.group) - groupRank(b.group) || a.order - b.order, + ); + operations.forEach((operation, index) => (operation.order = index)); + + const spec = JSON.stringify(served, null, 2); + return { + info: { + title: text(root.info.title), + version: text(root.info.version), + description: richText(root.info.description, file, warnings, "/info/description"), + servers: rootServers, + securitySchemes: schemes, + }, + operations, + spec, + specHash: createHash("sha256").update(spec).digest("hex"), + warnings: locateDiagnostics(input.text, warnings), + }; +} diff --git a/apps/api/src/documentation/openapi/parse.ts b/apps/api/src/documentation/openapi/parse.ts new file mode 100644 index 0000000..0a5afd3 --- /dev/null +++ b/apps/api/src/documentation/openapi/parse.ts @@ -0,0 +1,69 @@ +import { isMap, isSeq, LineCounter, parse, parseDocument, type Node } from "yaml"; +import { diagnostic, type DocsDiagnostic } from "@frontlit/text-editor/docs"; + +export const OPENAPI_LIMITS = { bytes: 5 * 1024 * 1024, operations: 500 }; + +export class OpenApiImportError extends Error { + constructor(readonly diagnostics: DocsDiagnostic[]) { + super("The OpenAPI spec is invalid."); + this.name = "OpenApiImportError"; + } +} + +/** Parses a JSON or YAML spec. YAML aliases are refused (no entity expansion). */ +export function parseSpecText(text: string, file: string): unknown { + try { + if (/\.json$/i.test(file)) return JSON.parse(text); + return parse(text, { maxAliasCount: 0, uniqueKeys: true }); + } catch (error) { + const position = (error as { linePos?: { line: number; col: number }[] }) + .linePos?.[0]; + throw new OpenApiImportError([ + diagnostic( + "OPENAPI_PARSE", + `The spec could not be parsed: ${(error as Error).message.split("\n")[0]}`, + { file, line: position?.line, column: position?.col }, + ), + ]); + } +} + +export const escapePointer = (segment: string | number) => + String(segment).replace(/~/g, "~0").replace(/\//g, "~1"); +export const pointer = (path: (string | number)[]) => + path.length ? `/${path.map(escapePointer).join("/")}` : ""; +const unescapePointer = (segment: string) => + segment.replace(/~1/g, "/").replace(/~0/g, "~"); + +/** Adds line/column to diagnostics with a JSON pointer, parsing the source + * again with positions only when there is something to locate. */ +export function locateDiagnostics(text: string, items: DocsDiagnostic[]) { + if (!items.some((item) => item.fieldPath && item.line === undefined)) return items; + const lineCounter = new LineCounter(); + const doc = parseDocument(text, { lineCounter, keepSourceTokens: false }); + return items.map((item) => { + if (!item.fieldPath || item.line !== undefined) return item; + let node: unknown = doc.contents; + let located: Node | null = null; + for (const segment of item.fieldPath.split("/").slice(1).map(unescapePointer)) { + if (isMap(node)) { + const pair = node.items.find( + (entry) => + String((entry.key as { value?: unknown })?.value ?? entry.key) === segment, + ); + if (!pair) break; + located = (pair.key as Node) ?? located; + node = pair.value; + } else if (isSeq(node)) { + const child = node.items[Number(segment)]; + if (!child) break; + located = child as Node; + node = child; + } else break; + } + const offset = located?.range?.[0]; + if (offset === undefined) return item; + const { line, col } = lineCounter.linePos(offset); + return { ...item, line, column: col }; + }); +} diff --git a/apps/api/src/documentation/openapi/samples.ts b/apps/api/src/documentation/openapi/samples.ts new file mode 100644 index 0000000..5ada040 --- /dev/null +++ b/apps/api/src/documentation/openapi/samples.ts @@ -0,0 +1,149 @@ +import type { + ApiCodeSample, + ApiPlaygroundAuth, + ApiPlaygroundModel, +} from "@frontlit/api-contract"; + +/** Base URL for samples: the first server with variable defaults filled in. */ +export function sampleBaseUrl(model: ApiPlaygroundModel) { + const server = model.servers[0]; + if (!server || !/^https?:\/\//i.test(resolveServerUrl(server.url, server.variables))) + return "{baseUrl}"; + return resolveServerUrl(server.url, server.variables).replace(/\/+$/, ""); +} + +export function resolveServerUrl( + url: string, + variables: { name: string; default: string }[], + values: Record = {}, +) { + return url.replace(/\{([^}]+)\}/g, (match, name: string) => { + const variable = variables.find((v) => v.name === name); + return values[name] ?? variable?.default ?? match; + }); +} + +function authHeaders(auth: ApiPlaygroundAuth[]) { + const headers: [string, string][] = []; + const query: [string, string][] = []; + for (const scheme of auth) { + if (scheme.kind === "bearer") headers.push(["Authorization", "Bearer $API_TOKEN"]); + else if (scheme.kind === "basic") + headers.push(["Authorization", "Basic $BASE64_CREDENTIALS"]); + else if (scheme.kind === "apiKey") { + const placeholder = `$${scheme.name.replace(/[^a-z0-9]+/gi, "_").toUpperCase()}`; + (scheme.in === "query" ? query : headers).push([scheme.name, placeholder]); + } + } + return { headers, query }; +} + +const quote = (value: string) => `'${value.replace(/'/g, `'\\''`)}'`; + +/** curl, JavaScript and Python samples for the operation's first auth option. */ +export function generateCodeSamples(model: ApiPlaygroundModel): ApiCodeSample[] { + const auth = authHeaders(model.auth[0] ?? []); + const required = model.parameters.filter((p) => p.required || p.in === "path"); + const query = [ + ...required + .filter((p) => p.in === "query") + .map((p) => [p.name, p.example ?? `{${p.name}}`] as [string, string]), + ...auth.query, + ]; + const headers = [ + ...required + .filter((p) => p.in === "header") + .map((p) => [p.name, p.example ?? `{${p.name}}`] as [string, string]), + ...auth.headers, + ]; + const search = query.length + ? `?${query.map(([k, v]) => `${encodeURIComponent(k)}=${v}`).join("&")}` + : ""; + const url = `${sampleBaseUrl(model)}${model.path}${search}`; + const method = model.method.toUpperCase(); + const body = model.body; + const json = body?.kind === "json" && body.example ? body.example : undefined; + if (json) headers.push(["Content-Type", body!.mediaType]); + const formFields = + body && (body.kind === "multipart" || body.kind === "form") + ? body.fields.filter((f) => f.required).length + ? body.fields.filter((f) => f.required) + : body.fields.slice(0, 3) + : []; + + const curl = [`curl -X ${method} ${quote(url)}`]; + for (const [name, value] of headers) curl.push(` -H ${quote(`${name}: ${value}`)}`); + if (json) curl.push(` -d ${quote(json)}`); + for (const field of formFields) + curl.push( + body!.kind === "multipart" + ? ` -F ${quote(`${field.name}=${field.binary ? "@file" : (field.example ?? "value")}`)}` + : ` --data-urlencode ${quote(`${field.name}=${field.example ?? "value"}`)}`, + ); + + const jsHeaders = headers.length + ? ` headers: {\n${headers.map(([k, v]) => ` ${JSON.stringify(k)}: ${JSON.stringify(v)},`).join("\n")}\n },\n` + : ""; + let jsBody = ""; + let jsPrelude = ""; + if (json) jsBody = ` body: JSON.stringify(${json.replace(/\n/g, "\n ")}),\n`; + else if (formFields.length && body!.kind === "multipart") { + jsPrelude = `const form = new FormData();\n${formFields + .map((f) => + f.binary + ? `form.append(${JSON.stringify(f.name)}, file);` + : `form.append(${JSON.stringify(f.name)}, ${JSON.stringify(f.example ?? "value")});`, + ) + .join("\n")}\n\n`; + jsBody = " body: form,\n"; + } else if (formFields.length) { + jsBody = ` body: new URLSearchParams(${JSON.stringify( + Object.fromEntries(formFields.map((f) => [f.name, f.example ?? "value"])), + )}),\n`; + } + const javascript = `${jsPrelude}const response = await fetch(${JSON.stringify(url)}, {\n method: ${JSON.stringify(method)},\n${jsHeaders}${jsBody}});\nconst data = await response.json();`; + + const pyHeaders = headers.length + ? ` headers={\n${headers.map(([k, v]) => ` ${JSON.stringify(k)}: ${JSON.stringify(v)},`).join("\n")}\n },\n` + : ""; + let pyBody = ""; + if (json) pyBody = ` json=${toPython(json)},\n`; + else if (formFields.length) { + const plain = formFields.filter((f) => !f.binary); + const files = formFields.filter((f) => f.binary); + if (plain.length) + pyBody += ` data={${plain.map((f) => `${JSON.stringify(f.name)}: ${JSON.stringify(f.example ?? "value")}`).join(", ")}},\n`; + if (files.length) + pyBody += ` files={${files.map((f) => `${JSON.stringify(f.name)}: open("file", "rb")`).join(", ")}},\n`; + } + const python = `import requests\n\nresponse = requests.request(\n ${JSON.stringify(method)},\n ${JSON.stringify(url)},\n${pyHeaders}${pyBody})\ndata = response.json()`; + + return [ + { label: "cURL", lang: "shell", source: curl.join(" \\\n") }, + { label: "JavaScript", lang: "javascript", source: javascript }, + { label: "Python", lang: "python", source: python }, + ]; +} + +function toPython(json: string) { + const literal = (value: unknown, indent: string): string => { + if (value === null) return "None"; + if (value === true) return "True"; + if (value === false) return "False"; + if (typeof value !== "object") return JSON.stringify(value); + const inner = `${indent} `; + if (Array.isArray(value)) + return value.length + ? `[\n${value.map((v) => `${inner}${literal(v, inner)},`).join("\n")}\n${indent}]` + : "[]"; + const entries = Object.entries(value); + return entries.length + ? `{\n${entries.map(([k, v]) => `${inner}${JSON.stringify(k)}: ${literal(v, inner)},`).join("\n")}\n${indent}}` + : "{}"; + }; + try { + return literal(JSON.parse(json), " "); + } catch { + return json; + } +} diff --git a/apps/api/src/documentation/openapi/schema.ts b/apps/api/src/documentation/openapi/schema.ts new file mode 100644 index 0000000..cda050a --- /dev/null +++ b/apps/api/src/documentation/openapi/schema.ts @@ -0,0 +1,192 @@ +/** Internal `$ref` handling and example generation for OpenAPI 3.0/3.1. */ + +type Json = any; + +export function resolvePointer(root: Json, ref: string): Json { + if (!ref.startsWith("#")) return undefined; + let node = root; + for (const raw of ref.slice(1).split("/").slice(1)) { + const key = decodeURIComponent(raw).replace(/~1/g, "/").replace(/~0/g, "~"); + if (node === null || typeof node !== "object") return undefined; + node = node[key]; + } + return node; +} + +/** Follows `$ref` chains of non-schema objects (parameters, bodies, responses). */ +export function deref(root: Json, value: Json, depth = 0): Json { + if ( + value && + typeof value === "object" && + typeof value.$ref === "string" && + depth < 20 + ) + return deref(root, resolvePointer(root, value.$ref), depth + 1); + return value; +} + +/** Every `#/components/schemas/*` the value references, transitively. */ +export function collectSchemaRefs( + root: Json, + value: Json, + into = new Map(), +) { + const visit = (node: Json) => { + if (Array.isArray(node)) return node.forEach(visit); + if (!node || typeof node !== "object") return; + if ( + typeof node.$ref === "string" && + node.$ref.startsWith("#/components/schemas/") + ) { + if (into.has(node.$ref)) return; + const target = resolvePointer(root, node.$ref); + into.set(node.$ref, target ?? {}); + visit(target); + } + for (const [key, child] of Object.entries(node)) if (key !== "$ref") visit(child); + }; + visit(value); + return into; +} + +const typeOf = (schema: Json): string | undefined => { + const type = schema?.type; + if (Array.isArray(type)) return type.find((t: string) => t !== "null") ?? type[0]; + return type; +}; + +function stringExample(format: string | undefined) { + switch (format) { + case "date-time": + return "2026-01-01T00:00:00Z"; + case "date": + return "2026-01-01"; + case "email": + return "user@example.com"; + case "uri": + case "url": + return "https://example.com"; + case "uuid": + return "3fa85f64-5717-4562-b3fc-2c963f66afa6"; + case "binary": + return ""; + default: + return "string"; + } +} + +/** A plausible example value for a schema: the spec's own example, default, + * const or first enum value when present, else built from the types. */ +export function sampleSchema( + root: Json, + schema: Json, + options: { direction: "request" | "response" }, + seen: string[] = [], +): Json { + if (!schema || typeof schema !== "object" || seen.length > 8) return null; + if (typeof schema.$ref === "string") { + if (seen.includes(schema.$ref)) return null; + return sampleSchema(root, resolvePointer(root, schema.$ref), options, [ + ...seen, + schema.$ref, + ]); + } + if (schema.example !== undefined) return schema.example; + if (Array.isArray(schema.examples) && schema.examples.length) + return schema.examples[0]; + if (schema.default !== undefined) return schema.default; + if (schema.const !== undefined) return schema.const; + if (Array.isArray(schema.enum) && schema.enum.length) return schema.enum[0]; + if (Array.isArray(schema.allOf)) { + const parts = schema.allOf.map((part: Json) => + sampleSchema(root, part, options, seen), + ); + return parts.every( + (part: Json) => part && typeof part === "object" && !Array.isArray(part), + ) + ? Object.assign({}, ...parts) + : (parts[0] ?? null); + } + for (const key of ["oneOf", "anyOf"]) + if (Array.isArray(schema[key]) && schema[key].length) + return sampleSchema(root, schema[key][0], options, seen); + const type = typeOf(schema) ?? (schema.properties ? "object" : undefined); + switch (type) { + case "object": { + const result: Record = {}; + for (const [name, property] of Object.entries(schema.properties ?? {})) { + const resolved = property?.$ref + ? resolvePointer(root, property.$ref) + : property; + if (options.direction === "request" && resolved?.readOnly) continue; + if (options.direction === "response" && resolved?.writeOnly) continue; + result[name] = sampleSchema(root, property, options, seen); + } + return result; + } + case "array": + return schema.items ? [sampleSchema(root, schema.items, options, seen)] : []; + case "string": + return stringExample(schema.format); + case "integer": + case "number": + return typeof schema.minimum === "number" ? schema.minimum : 0; + case "boolean": + return true; + default: + return null; + } +} + +/** The media type's own example if it has one, else one generated from its schema. */ +export function mediaExample( + root: Json, + media: Json, + direction: "request" | "response", +): Json { + if (media?.example !== undefined) return media.example; + const first = media?.examples && Object.values(media.examples)[0]; + const value = deref(root, first); + if (value?.value !== undefined) return value.value; + return media?.schema ? sampleSchema(root, media.schema, { direction }) : undefined; +} + +/** Top-level properties of an object schema, following `$ref` and `allOf`. */ +export function objectProperties( + root: Json, + schema: Json, + seen: string[] = [], +): { properties: Record; required: string[] } { + if (!schema || typeof schema !== "object" || seen.length > 8) + return { properties: {}, required: [] }; + if (typeof schema.$ref === "string") { + if (seen.includes(schema.$ref)) return { properties: {}, required: [] }; + return objectProperties(root, resolvePointer(root, schema.$ref), [ + ...seen, + schema.$ref, + ]); + } + const result = { + properties: { ...(schema.properties ?? {}) } as Record, + required: [...(Array.isArray(schema.required) ? schema.required : [])] as string[], + }; + for (const part of Array.isArray(schema.allOf) ? schema.allOf : []) { + const merged = objectProperties(root, part, seen); + Object.assign(result.properties, merged.properties); + result.required.push(...merged.required); + } + return result; +} + +export function isBinarySchema(root: Json, schema: Json): boolean { + const resolved = schema?.$ref ? resolvePointer(root, schema.$ref) : schema; + if (!resolved) return false; + if (resolved.format === "binary" || resolved.format === "base64") return true; + if (resolved.contentMediaType || resolved.contentEncoding) return true; + return typeOf(resolved) === "array" && isBinarySchema(root, resolved.items); +} + +export function enumValues(root: Json, schema: Json): string[] | undefined { + const resolved = schema?.$ref ? resolvePointer(root, schema.$ref) : schema; + return Array.isArray(resolved?.enum) ? resolved.enum.map(String) : undefined; +} diff --git a/apps/api/src/documentation/openapi/service.test.ts b/apps/api/src/documentation/openapi/service.test.ts new file mode 100644 index 0000000..d654b98 --- /dev/null +++ b/apps/api/src/documentation/openapi/service.test.ts @@ -0,0 +1,155 @@ +import { createHash } from "node:crypto"; +import { readFileSync } from "node:fs"; +import { join } from "node:path"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { eq } from "drizzle-orm"; +import { makeTestDb, seedTeam, truncateAll, type TestDb } from "../../test/db"; +vi.mock("../../db/client", async () => ({ db: await makeTestDb() })); +import { db as appDb } from "../../db/client"; +import { + documentationApiSpecs as specs, + documentationDeliveryOutbox as outbox, + documentationGitBindings as bindings, + documentationSettings as settings, +} from "../../db/schema"; +import type { GitTreeEntry } from "../github/importer"; +import { + getApiReferenceStatus, + readApiOperation, + readApiReference, + readApiSpec, + searchApiReference, + syncApiReference, +} from "./service"; +const db = appDb as unknown as TestDb; + +const medialit = readFileSync(join(__dirname, "fixtures", "medialit.json"), "utf8"); +const sha = (n: number) => String(n).padStart(40, "0"); + +function repo(files: Record) { + const blobs = new Map(); + const entries: GitTreeEntry[] = Object.entries(files).map(([path, text]) => { + const blob = createHash("sha1").update(text).digest("hex"); + blobs.set(blob, text); + return { path, mode: "100644", type: "blob", sha: blob, size: text.length }; + }); + return { + readTree: async () => entries, + readBlob: async (blob: string) => new TextEncoder().encode(blobs.get(blob)!), + }; +} + +async function seed(apiSpecPath: string | null = "apps/api/swagger.json") { + const { account, team } = await seedTeam(db); + const [binding] = await db + .insert(bindings) + .values({ + teamId: team.id, + installationId: "7", + repositoryId: "9", + ownerId: "8", + ownerLogin: "org", + repositoryName: "docs", + repositoryPrivate: true, + branch: "main", + directory: "frontlit/docs", + status: "connected", + grantActorAccountId: account.id, + grantGithubUserId: "123", + repositoryAdminVerified: true, + capabilities: { read: true, proposals: false, directCommit: false }, + apiSpecPath, + apiMount: "api", + }) + .returning(); + return { team, binding: binding! }; +} + +describe("API reference sync", () => { + beforeEach(async () => truncateAll(db)); + + it("publishes the spec, skips it when unchanged and keeps it live on errors", async () => { + const { team, binding } = await seed(); + await syncApiReference({ + teamId: team.id, + binding, + head: sha(100), + ...repo({ "apps/api/swagger.json": medialit }), + }); + const reference = await readApiReference(team.id); + expect(reference?.mount).toBe("api"); + expect(reference?.groups.flatMap((g) => g.operations)).toHaveLength(11); + const route = reference!.groups[0].operations[0].route; + expect((await readApiOperation(team.id, route))?.operation.title).toBeTruthy(); + expect(JSON.parse((await readApiSpec(team.id))!.spec).openapi).toBe("3.0.3"); + expect( + (await db.query.documentationDeliveryOutbox.findMany()).map((e) => e.type), + ).toEqual(["api_reference"]); + + // Same blob: nothing is re-imported. + const live = (await db.query.documentationSettings.findFirst())!.activeApiSpecId; + await syncApiReference({ + teamId: team.id, + binding, + head: sha(101), + ...repo({ "apps/api/swagger.json": medialit }), + }); + expect((await db.query.documentationSettings.findFirst())!.activeApiSpecId).toBe( + live, + ); + + // A broken spec is recorded and the live one stays. + await syncApiReference({ + teamId: team.id, + binding, + head: sha(102), + ...repo({ "apps/api/swagger.json": "{ not json" }), + }); + const status = await getApiReferenceStatus(team.id); + expect(status.live?.operationCount).toBe(11); + expect(status.lastFailure?.commitSha).toBe(sha(102)); + expect(status.diagnostics[0].code).toBe("OPENAPI_PARSE"); + + // A moved file is reported, not unpublished. + await syncApiReference({ + teamId: team.id, + binding, + head: sha(103), + ...repo({ "other.json": medialit }), + }); + expect((await getApiReferenceStatus(team.id)).diagnostics[0].code).toBe( + "OPENAPI_NOT_FOUND", + ); + expect(await readApiReference(team.id)).not.toBeNull(); + expect(await db.select().from(specs).where(eq(specs.teamId, team.id))).toHaveLength( + 2, + ); + }); + + it("finds operations in search", async () => { + const { team, binding } = await seed(); + await syncApiReference({ + teamId: team.id, + binding, + head: sha(100), + ...repo({ "apps/api/swagger.json": medialit }), + }); + const results = await searchApiReference(team.id, "upload file"); + expect(results.length).toBeGreaterThan(0); + expect(results[0]).toMatchObject({ kind: "api-operation" }); + expect(results[0].url).toContain("/docs/api/"); + }); + + it("does nothing without a spec path", async () => { + const { team, binding } = await seed(null); + await syncApiReference({ + teamId: team.id, + binding, + head: sha(100), + ...repo({ "apps/api/swagger.json": medialit }), + }); + expect(await readApiReference(team.id)).toBeNull(); + expect(await db.select().from(outbox)).toHaveLength(0); + expect(await db.select().from(settings)).toHaveLength(0); + }); +}); diff --git a/apps/api/src/documentation/openapi/service.ts b/apps/api/src/documentation/openapi/service.ts new file mode 100644 index 0000000..2fda6c1 --- /dev/null +++ b/apps/api/src/documentation/openapi/service.ts @@ -0,0 +1,530 @@ +import { and, asc, eq, ne, or, sql } from "drizzle-orm"; +import { diagnostic, type DocsDiagnostic } from "@frontlit/text-editor/docs"; +import type { ApiOperationSlice, ApiReferenceInfo } from "@frontlit/api-contract"; +import { db } from "../../db/client"; +import { + documentationApiOperations as operations, + documentationApiSpecs as specs, + documentationDeliveryOutbox as outbox, + documentationGitBindings as bindings, + documentationMemberships as memberships, + documentationSettings as settingsTable, +} from "../../db/schema"; +import type { AuthzContext } from "../../auth/authorize"; +import { lockSettings } from "../authoring"; +import { DocsGitHubApp, getDocsGitHubAppConfig, GitHubDocsError } from "../github/app"; +import type { GitTreeEntry } from "../github/importer"; +import { requireGitManager } from "../github/setup"; +import { enqueueGitSync, readGitBindingAccess } from "../github/state"; +import { siteOriginForTeam } from "../../utils/site-url"; +import { importOpenApiSpec, normalizeApiMount, type ImportedSpec } from "./import"; +import { OPENAPI_LIMITS, OpenApiImportError } from "./parse"; + +type Executor = any; +type Binding = typeof bindings.$inferSelect; + +/** Mounts that would shadow built-in docs routes. */ +const RESERVED_MOUNTS = new Set(["collections", "search", "index"]); + +const decoder = new TextDecoder("utf-8", { fatal: true }); + +/** A clean repo-relative file path, or null when it isn't one. */ +export function normalizeSpecPath(value: string | null | undefined) { + const path = (value ?? "") + .trim() + .replace(/^\.\/+/, "") + .replace(/\\/g, "/"); + if (!path) return null; + if ( + path.startsWith("/") || + path.split("/").some((part) => !part || part === "." || part === "..") || + !/\.(json|ya?ml)$/i.test(path) + ) + throw new GitHubDocsError("API_SPEC_PATH_INVALID", 400); + return path; +} + +function asWarning(item: DocsDiagnostic): DocsDiagnostic { + return item.severity === "warning" + ? item + : { ...item, severity: "warning", blocksSave: false, blocksPublish: false }; +} + +/** Imports the spec at `commitSha` without storing anything. */ +export async function readSpecFromTree(input: { + specPath: string; + mount: string; + entries: GitTreeEntry[]; + readBlob: (sha: string) => Promise; +}): Promise<{ blobSha: string | null; result: ImportedSpec }> { + const entry = input.entries.find( + (e) => e.path === input.specPath && e.type === "blob" && e.mode !== "120000", + ); + if (!entry) + throw new OpenApiImportError([ + diagnostic( + "OPENAPI_NOT_FOUND", + `No spec file at ${input.specPath} on this branch. Update the spec path if it moved.`, + { file: input.specPath }, + ), + ]); + if ((entry.size ?? 0) > OPENAPI_LIMITS.bytes) + throw new OpenApiImportError([ + diagnostic("OPENAPI_LIMIT", "The spec is larger than 5 MiB.", { + file: input.specPath, + }), + ]); + let text: string; + try { + text = decoder.decode(await input.readBlob(entry.sha)); + } catch (error) { + if (error instanceof GitHubDocsError) throw error; + throw new OpenApiImportError([ + diagnostic("OPENAPI_PARSE", "The spec isn't valid UTF-8 text.", { + file: input.specPath, + }), + ]); + } + return { + blobSha: entry.sha, + result: importOpenApiSpec({ text, file: input.specPath, mount: input.mount }), + }; +} + +async function insertEvent(tx: Executor, teamId: string) { + const settings = await tx.query.documentationSettings.findFirst({ + where: eq(settingsTable.teamId, teamId), + }); + await tx.insert(outbox).values({ + teamId, + publicationId: settings?.activePublicationId ?? null, + policyEpoch: settings?.policyEpoch ?? 0, + type: "api_reference", + payload: { apiSpecId: settings?.activeApiSpecId ?? null }, + destinations: process.env.REVALIDATE_WEBHOOK_SECRET + ? ["local", "sites"] + : ["local"], + }); +} + +async function recordFailure( + teamId: string, + input: { commitSha: string; specPath: string; mount: string }, + diagnostics: DocsDiagnostic[], +) { + await db.transaction(async (tx) => { + await lockSettings(tx, teamId); + await tx + .delete(specs) + .where(and(eq(specs.teamId, teamId), eq(specs.status, "failed"))); + await tx.insert(specs).values({ + teamId, + status: "failed", + commitSha: input.commitSha, + sourcePath: input.specPath, + mount: input.mount, + diagnostics: diagnostics as any, + }); + }); +} + +/** Replaces the live API reference in one transaction. Returns false when + * the binding changed meanwhile (a newer sync will import again). */ +async function activateSpec( + teamId: string, + input: { + commitSha: string; + specPath: string; + mount: string; + generation: number; + blobSha: string | null; + }, + imported: ImportedSpec, +) { + return db.transaction(async (tx) => { + await lockSettings(tx, teamId); + const binding = await tx.query.documentationGitBindings.findFirst({ + where: eq(bindings.teamId, teamId), + }); + if ( + !binding || + binding.generation !== input.generation || + binding.apiSpecPath !== input.specPath || + binding.apiMount !== input.mount + ) + return false; + await tx.delete(specs).where(eq(specs.teamId, teamId)); + const [spec] = await tx + .insert(specs) + .values({ + teamId, + status: "active", + commitSha: input.commitSha, + sourcePath: input.specPath, + sourceBlobSha: input.blobSha, + mount: input.mount, + spec: imported.spec, + specHash: imported.specHash, + info: imported.info as any, + diagnostics: imported.warnings as any, + }) + .returning(); + if (imported.operations.length) + await tx.insert(operations).values( + imported.operations.map((operation) => ({ + specId: spec.specId, + teamId, + route: operation.route, + title: operation.title, + group: operation.group, + order: operation.order, + method: operation.method, + path: operation.path, + deprecated: operation.deprecated, + slice: JSON.stringify(operation.slice), + chunks: operation.chunks, + })), + ); + await tx + .update(settingsTable) + .set({ activeApiSpecId: spec.specId }) + .where(eq(settingsTable.teamId, teamId)); + await insertEvent(tx, teamId); + return true; + }); +} + +/** + * The API reference step of a Git sync (docs/openapi-support.md §6.2). It + * runs before pages and docs and never fails the sync: problems are stored + * on a `failed` row and the live API reference stays as it is. + */ +export async function syncApiReference(input: { + teamId: string; + binding: Binding; + head: string; + readTree: () => Promise; + readBlob: (sha: string) => Promise; +}) { + const { teamId, binding, head } = input; + if (!binding.apiSpecPath) return; + const target = { + commitSha: head, + specPath: binding.apiSpecPath, + mount: binding.apiMount, + }; + try { + const entries = await input.readTree(); + const live = await activeSpecRow(teamId); + const entry = entries.find((e) => e.path === target.specPath && e.type === "blob"); + if ( + live && + entry && + live.sourceBlobSha === entry.sha && + live.sourcePath === target.specPath && + live.mount === target.mount + ) { + // Unchanged; a stale failure from an earlier commit no longer applies. + await db + .delete(specs) + .where(and(eq(specs.teamId, teamId), eq(specs.status, "failed"))); + return; + } + const { blobSha, result } = await readSpecFromTree({ + specPath: target.specPath, + mount: target.mount, + entries, + readBlob: input.readBlob, + }); + await activateSpec( + teamId, + { ...target, generation: binding.generation, blobSha }, + result, + ); + } catch (error) { + if (error instanceof OpenApiImportError) { + await recordFailure(teamId, target, error.diagnostics); + return; + } + // A GitHub or database problem: the docs and pages steps that follow + // report it; the live API reference stays. + console.error("API reference sync failed", error); + } +} + +async function activeSpecRow(teamId: string) { + const settings = await db.query.documentationSettings.findFirst({ + where: eq(settingsTable.teamId, teamId), + }); + if (!settings?.activeApiSpecId) return null; + return ( + (await db.query.documentationApiSpecs.findFirst({ + where: and( + eq(specs.specId, settings.activeApiSpecId), + eq(specs.teamId, teamId), + eq(specs.status, "active"), + ), + })) ?? null + ); +} + +/** Mount problems: reserved names, or docs pages already published under it. */ +async function mountConflict(teamId: string, mount: string) { + if (RESERVED_MOUNTS.has(mount)) return `/docs/${mount} is reserved.`; + const settings = await db.query.documentationSettings.findFirst({ + where: eq(settingsTable.teamId, teamId), + }); + if (!settings?.activePublicationId) return null; + const clash = await db.query.documentationMemberships.findFirst({ + where: and( + eq(memberships.publicationId, settings.activePublicationId), + or(eq(memberships.route, mount), sql`${memberships.route} like ${`${mount}/%`}`), + ), + }); + return clash ? `Docs pages are already published under /docs/${mount}.` : null; +} + +async function connectedBinding(teamId: string) { + const binding = await db.query.documentationGitBindings.findFirst({ + where: eq(bindings.teamId, teamId), + }); + if (binding?.status !== "connected") + throw new GitHubDocsError("SOURCE_REQUIRES_GIT", 409); + return binding; +} + +/** Dry run of a spec path at the branch head, for the admin settings. */ +export async function previewApiReference( + teamId: string, + actor: AuthzContext, + input: { specPath: string | null; mount?: string }, + app = new DocsGitHubApp(getDocsGitHubAppConfig()), +) { + requireGitManager(teamId, actor); + const binding = await connectedBinding(teamId); + const specPath = normalizeSpecPath(input.specPath); + const mount = normalizeApiMount(input.mount ?? binding.apiMount); + if (!specPath) throw new GitHubDocsError("API_SPEC_PATH_INVALID", 400); + const access = await readGitBindingAccess(binding, app); + const owner = access.repository.owner.login; + const name = access.repository.name; + const commitSha = await app.branchHead(access.token, owner, name, binding.branch); + const conflict = await mountConflict(teamId, mount); + const base = { commitSha, specPath, mount }; + try { + const { result } = await readSpecFromTree({ + specPath, + mount, + entries: await app.commitTree(access.token, owner, name, commitSha), + readBlob: (sha) => app.blob(access.token, owner, name, sha), + }); + const diagnostics = [...result.warnings]; + if (conflict) + diagnostics.unshift( + diagnostic("ROUTE_COLLISION", conflict, { route: `/docs/${mount}` }), + ); + return { + ...base, + publishable: !conflict, + title: result.info.title, + routes: result.operations.map((o) => ({ + route: `/docs/${o.route}`, + method: o.method, + path: o.path, + })), + diagnostics, + }; + } catch (error) { + if (!(error instanceof OpenApiImportError)) throw error; + return { + ...base, + publishable: false, + title: null, + routes: [], + diagnostics: error.diagnostics, + }; + } +} + +/** Saves the spec path and URL path and syncs right away. A null spec path + * unpublishes the API reference and deletes its rows. */ +export async function updateApiReferenceSettings( + teamId: string, + actor: AuthzContext, + input: { specPath: string | null; mount?: string }, +) { + requireGitManager(teamId, actor, true); + const binding = await connectedBinding(teamId); + const specPath = normalizeSpecPath(input.specPath); + const mount = normalizeApiMount(input.mount ?? binding.apiMount); + if (specPath && (await mountConflict(teamId, mount))) + throw new GitHubDocsError("API_MOUNT_CONFLICT", 409); + await db.transaction(async (tx) => { + await lockSettings(tx, teamId); + await tx + .update(bindings) + .set({ apiSpecPath: specPath, apiMount: mount, updatedAt: new Date() }) + .where(eq(bindings.teamId, teamId)); + if (!specPath) { + const settings = await tx.query.documentationSettings.findFirst({ + where: eq(settingsTable.teamId, teamId), + }); + await tx.delete(specs).where(eq(specs.teamId, teamId)); + await tx + .update(settingsTable) + .set({ activeApiSpecId: null }) + .where(eq(settingsTable.teamId, teamId)); + if (settings?.activeApiSpecId) await insertEvent(tx, teamId); + return; + } + await enqueueGitSync(teamId, { trigger: "configuration" }, tx); + }); + return getApiReferenceStatus(teamId); +} + +export async function getApiReferenceStatus(teamId: string) { + const binding = await db.query.documentationGitBindings.findFirst({ + where: eq(bindings.teamId, teamId), + }); + const rows = await db.query.documentationApiSpecs.findMany({ + where: eq(specs.teamId, teamId), + columns: { spec: false }, + }); + const live = await activeSpecRow(teamId); + const failed = rows.find((row) => row.status === "failed") ?? null; + const count = live + ? ( + await db + .select({ count: sql`count(*)::int` }) + .from(operations) + .where(eq(operations.specId, live.specId)) + )[0].count + : 0; + const info = live?.info as unknown as ApiReferenceInfo | undefined; + return { + specPath: binding?.apiSpecPath ?? null, + mount: binding?.apiMount ?? "api-reference", + live: live + ? { + commitSha: live.commitSha, + sourcePath: live.sourcePath, + title: info?.title ?? "", + version: info?.version ?? "", + operationCount: count, + createdAt: live.createdAt, + } + : null, + lastFailure: failed + ? { + commitSha: failed.commitSha, + sourcePath: failed.sourcePath, + createdAt: failed.createdAt, + } + : null, + diagnostics: ((failed ?? live)?.diagnostics ?? []) as unknown as DocsDiagnostic[], + }; +} + +// --------------------------------------------------------------------------- +// Public reads +// --------------------------------------------------------------------------- + +export async function readApiReference(teamId: string) { + const live = await activeSpecRow(teamId); + if (!live) return null; + const rows = await db + .select({ + route: operations.route, + title: operations.title, + group: operations.group, + method: operations.method, + path: operations.path, + deprecated: operations.deprecated, + }) + .from(operations) + .where(eq(operations.specId, live.specId)) + .orderBy(asc(operations.order)); + const groups: { name: string; operations: Omit<(typeof rows)[number], "group">[] }[] = + []; + for (const { group, ...operation } of rows) { + let entry = groups.at(-1); + if (entry?.name !== group) groups.push((entry = { name: group, operations: [] })); + entry.operations.push(operation); + } + return { + mount: live.mount, + commitSha: live.commitSha, + info: live.info as unknown as ApiReferenceInfo, + groups, + }; +} + +export async function readApiOperation(teamId: string, route: string) { + const live = await activeSpecRow(teamId); + if (!live) return null; + const row = await db.query.documentationApiOperations.findFirst({ + where: and(eq(operations.specId, live.specId), eq(operations.route, route)), + }); + if (!row) return null; + return { + route: row.route, + mount: live.mount, + operation: JSON.parse(row.slice) as ApiOperationSlice, + }; +} + +export async function readApiSpec(teamId: string) { + const live = await activeSpecRow(teamId); + if (!live?.spec) return null; + return { spec: live.spec, hash: live.specHash ?? "" }; +} + +/** Keyword search over the live operations' chunks. Operations aren't + * embedded yet (docs/openapi-support.md §10), so they always match lexically. */ +export async function searchApiReference(teamId: string, query: string, k = 6) { + const settings = await db.query.documentationSettings.findFirst({ + where: eq(settingsTable.teamId, teamId), + }); + if (!settings?.activeApiSpecId || settings.indexing?.enabled === false) return []; + const tokens = [...new Set(query.toLowerCase().match(/[\p{L}\p{N}]+/gu) ?? [])]; + if (!tokens.length) return []; + const rows = await db + .select({ + route: operations.route, + title: operations.title, + chunks: operations.chunks, + }) + .from(operations) + .where(eq(operations.specId, settings.activeApiSpecId)); + const origin = await siteOriginForTeam(teamId); + const floor = settings.indexing?.mode === "semantic" ? 0.5 : 0; + return rows + .flatMap((row) => + row.chunks.map((chunk) => { + const text = + `${row.title} ${chunk.heading ?? ""} ${chunk.content}`.toLowerCase(); + return { + documentId: row.route, + slug: row.route, + title: row.title, + ...chunk, + similarity: + tokens.filter((token) => text.includes(token)).length / tokens.length, + kind: "api-operation" as const, + url: `${origin}/docs/${row.route}${chunk.anchor ? `#${encodeURIComponent(chunk.anchor)}` : ""}`, + }; + }), + ) + .filter((c) => c.similarity > floor) + .sort((a, b) => b.similarity - a.similarity) + .slice(0, k); +} + +/** Routes under the API mount, for docs route validation. */ +export async function apiMountFor(teamId: string) { + const binding = await db.query.documentationGitBindings.findFirst({ + where: and(eq(bindings.teamId, teamId), ne(bindings.status, "disconnected")), + columns: { apiSpecPath: true, apiMount: true }, + }); + return binding?.apiSpecPath ? binding.apiMount : null; +} diff --git a/apps/api/src/documentation/queries.ts b/apps/api/src/documentation/queries.ts index f467fb5..414f09a 100644 --- a/apps/api/src/documentation/queries.ts +++ b/apps/api/src/documentation/queries.ts @@ -1,4 +1,5 @@ import { searchPublishedDocumentation } from "./publications"; +import { searchApiReference } from "./openapi/service"; import { DocsContentError, diagnostic } from "@frontlit/text-editor/docs"; import { lockSettings, DraftConflict } from "./authoring"; import { withDbTransaction } from "../db/transaction"; @@ -85,7 +86,7 @@ export interface SearchResult { anchor: string | null; content: string; similarity: number; - kind: "blog" | "doc" | "changelog"; + kind: "blog" | "doc" | "changelog" | "api-operation"; publicationId?: string | null; policyEpoch?: number; revisionId?: string; @@ -104,7 +105,12 @@ export async function searchChunks( query: string, k = 6, ): Promise { - const documentationResults = await searchPublishedDocumentation(teamId, query, k); + const documentationResults = [ + ...(await searchPublishedDocumentation(teamId, query, k)), + ...(await searchApiReference(teamId, query, k)), + ] + .sort((a, b) => b.similarity - a.similarity) + .slice(0, k); const others = await db .select({ id: documentChunks.id }) .from(documentChunks) diff --git a/apps/api/src/inbox-ai/generate.ts b/apps/api/src/inbox-ai/generate.ts index 31bc177..b8a95cc 100644 --- a/apps/api/src/inbox-ai/generate.ts +++ b/apps/api/src/inbox-ai/generate.ts @@ -54,7 +54,7 @@ export type InboxAiResult = { slug: string; title: string | null; anchor: string | null; - kind?: "blog" | "doc" | "changelog"; + kind?: "blog" | "doc" | "changelog" | "api-operation"; revisionId?: string; publicationId?: string | null; policyEpoch?: number; @@ -90,7 +90,7 @@ function toRetrieval( heading: string | null; anchor: string | null; similarity: number; - kind?: "blog" | "doc" | "changelog"; + kind?: "blog" | "doc" | "changelog" | "api-operation"; revisionId?: string; publicationId?: string | null; policyEpoch?: number; diff --git a/apps/api/src/public/routes.ts b/apps/api/src/public/routes.ts index 738b40c..7c8a58e 100644 --- a/apps/api/src/public/routes.ts +++ b/apps/api/src/public/routes.ts @@ -13,6 +13,11 @@ import { listPublishedDocuments, } from "../documents/queries"; import { searchChunks } from "../documentation/queries"; +import { + readApiOperation, + readApiReference, + readApiSpec, +} from "../documentation/openapi/service"; import { getChatbotSettings } from "../chatbot/queries"; import { findTeamById, findTeamByPublicId } from "../team/queries"; import { runInboxAi } from "../inbox-ai/generate"; @@ -139,7 +144,11 @@ const ticketThreadLimiter = postgresPublicLimit({ * draft-leak risk even without an explicit field-level strip. */ const router = Router(); router.use((req, res, next) => { - if (req.path.includes("/content/documentation") || req.path.endsWith("/sitemap")) + if ( + req.path.includes("/content/documentation") || + req.path.includes("/content/api-reference") || + req.path.endsWith("/sitemap") + ) res.setHeader("Cache-Control", "no-store"); next(); }); @@ -423,6 +432,18 @@ const impl = s.router(contract.public, { lastmod: (d.updatedAt ?? d.publishedAt)?.toISOString() ?? null, })), ]; + const apiReference = await readApiReference(team.id); + if (apiReference) + items.push( + { kind: "doc", path: `/docs/${apiReference.mount}`, lastmod: null }, + ...apiReference.groups.flatMap((group) => + group.operations.map((operation) => ({ + kind: "doc" as const, + path: `/docs/${operation.route}`, + lastmod: null, + })), + ), + ); if (changelogs.length > 0) { const lastmod = changelogs.reduce( (latest, d) => @@ -531,6 +552,18 @@ const impl = s.router(contract.public, { }, }; }, + getApiReference: async ({ params }) => { + const team = await findTeamByPublicId(params.teamId); + const reference = team ? await readApiReference(team.id) : null; + if (!reference) return { status: 404, body: { error: "Not found" } }; + return { status: 200, body: reference }; + }, + getApiOperation: async ({ params, query }) => { + const team = await findTeamByPublicId(params.teamId); + const operation = team ? await readApiOperation(team.id, query.route) : null; + if (!operation) return { status: 404, body: { error: "Not found" } }; + return { status: 200, body: operation }; + }, searchDocumentation: { middleware: [docsSearchLimiter as any], handler: async ({ params, query }) => { @@ -997,6 +1030,27 @@ const impl = s.router(contract.public, { createExpressEndpoints(contract.public, impl, router); +/** The live API reference's spec as committed (YAML converted to JSON, + * hidden operations removed); served by the public site at /openapi.json. */ +router.get( + "/public/:teamId/content/api-reference/openapi.json", + publicReadLimiter, + async (req, res) => { + const team = await findTeamByPublicId(req.params.teamId); + const spec = team ? await readApiSpec(team.id) : null; + if (!spec) { + res.status(404).json({ error: "Not found" }); + return; + } + res.setHeader("ETag", `"${spec.hash}"`); + if (req.headers["if-none-match"] === `"${spec.hash}"`) { + res.status(304).end(); + return; + } + res.type("application/json").send(spec.spec); + }, +); + /** Caddy on-demand TLS `ask` adapter. Caddy sends `GET ?domain=` and * issues a certificate only when this returns 200. Same host authority as * `GET /public/resolve-host`, so redirecting hosts (the other half of a diff --git a/apps/api/src/tickets/notify.ts b/apps/api/src/tickets/notify.ts index 3610aa3..d13889d 100644 --- a/apps/api/src/tickets/notify.ts +++ b/apps/api/src/tickets/notify.ts @@ -18,7 +18,7 @@ export type TicketMailSource = { slug: string; title: string | null; anchor?: string | null; - kind?: "blog" | "doc" | "changelog"; + kind?: "blog" | "doc" | "changelog" | "api-operation"; }; function uniqueMailSources(sources: TicketMailSource[]): TicketMailSource[] { diff --git a/apps/sites/app/api/webhooks/documentation/route.ts b/apps/sites/app/api/webhooks/documentation/route.ts index 63ef89c..c7babcc 100644 --- a/apps/sites/app/api/webhooks/documentation/route.ts +++ b/apps/sites/app/api/webhooks/documentation/route.ts @@ -4,7 +4,7 @@ import { verifyWebhookSignature } from "@/lib/webhook-signing"; const eventSchema = z .object({ eventId: z.string().regex(/^evt_[a-z0-9]+$/), - type: z.enum(["activation", "policy", "purge"]), + type: z.enum(["activation", "policy", "purge", "api_reference"]), teamId: z.string().regex(/^team_[a-z0-9]+$/), publicationId: z .string() diff --git a/apps/sites/app/sites/[teamId]/docs/[...slug]/page.tsx b/apps/sites/app/sites/[teamId]/docs/[...slug]/page.tsx index 3f7e321..987a2d8 100644 --- a/apps/sites/app/sites/[teamId]/docs/[...slug]/page.tsx +++ b/apps/sites/app/sites/[teamId]/docs/[...slug]/page.tsx @@ -1,25 +1,81 @@ import type { Metadata, ResolvingMetadata } from "next"; import { notFound } from "next/navigation"; import { extractDocHeadings } from "@/lib/extract-doc-headings"; -import { getDocumentationBySlug, getSettings } from "@/lib/api"; +import { + getApiOperation, + getApiReference, + getDocumentationBySlug, + getSettings, +} from "@/lib/api"; import { resolveSeo } from "@/lib/seo"; import { getCanonicalHost, isNoindex } from "@/lib/request-host"; import { serializeJsonLd, techArticleJsonLd } from "@/lib/json-ld"; import { API_PUBLIC_URL } from "@/lib/config"; import { SiteChrome } from "@/components/site-chrome"; import { ThemedDocArticle } from "@/components/themed-docs"; -import { collectionHref, loadDocsTree } from "@/lib/docs-tree"; +import { + ThemedApiLanding, + ThemedApiOperation, +} from "@/components/api-reference/api-reference"; +import { API_REFERENCE_LABEL, collectionHref, loadDocsTree } from "@/lib/docs-tree"; interface Props { params: Promise<{ teamId: string; slug: string[] }>; } +/** Paths under the API reference's mount belong to it, not to docs pages. */ +async function apiTarget(teamId: string, slug: string) { + const reference = await getApiReference(teamId); + if (!reference) return null; + if (slug === reference.mount) return { reference, operation: null }; + if (!slug.startsWith(`${reference.mount}/`)) return null; + const found = await getApiOperation(teamId, slug); + return { reference, operation: found?.operation ?? null, missing: !found }; +} + +async function apiMetadata( + slug: string, + title: string, + description: string | undefined, +): Promise { + const url = `https://${await getCanonicalHost()}/docs/${slug}`; + const index = !(await isNoindex()); + return { + title, + description, + alternates: { canonical: url }, + openGraph: { url, title, description }, + robots: { index, follow: index }, + }; +} + export async function generateMetadata( { params }: Props, parent: ResolvingMetadata, ): Promise { const { teamId, slug: segments } = await params; const slug = segments.join("/"); + const api = await apiTarget(teamId, slug); + if (api) { + if (!api.operation) + return api.missing + ? {} + : apiMetadata( + slug, + `${API_REFERENCE_LABEL} – ${api.reference.info.title}`, + undefined, + ); + const text = + api.operation.description?.kind === "text" + ? api.operation.description.text + : undefined; + return apiMetadata( + slug, + `${api.operation.title} – ${API_REFERENCE_LABEL}`, + text?.slice(0, 200) ?? + `${api.operation.method.toUpperCase()} ${api.operation.path}`, + ); + } const [doc, settings, previous] = await Promise.all([ getDocumentationBySlug(teamId, slug), getSettings(teamId), @@ -56,6 +112,32 @@ export async function generateMetadata( export default async function DocPage({ params }: Props) { const { teamId, slug: segments } = await params; const slug = segments.join("/"); + const api = await apiTarget(teamId, slug); + if (api) { + if (api.missing) notFound(); + const collections = await loadDocsTree(teamId); + return ( + + {api.operation ? ( + + ) : ( + + )} + + ); + } const [doc, collections] = await Promise.all([ getDocumentationBySlug(teamId, slug), loadDocsTree(teamId), diff --git a/apps/sites/app/sites/[teamId]/docs/collections/[sectionSlug]/page.tsx b/apps/sites/app/sites/[teamId]/docs/collections/[sectionSlug]/page.tsx index c81ab91..55c6885 100644 --- a/apps/sites/app/sites/[teamId]/docs/collections/[sectionSlug]/page.tsx +++ b/apps/sites/app/sites/[teamId]/docs/collections/[sectionSlug]/page.tsx @@ -1,5 +1,5 @@ import type { Metadata, ResolvingMetadata } from "next"; -import { notFound } from "next/navigation"; +import { notFound, redirect } from "next/navigation"; import { getSettings } from "@/lib/api"; import { resolveSeo } from "@/lib/seo"; import { getCanonicalHost } from "@/lib/request-host"; @@ -45,6 +45,7 @@ export default async function DocCollectionPage({ params }: Props) { const collections = await loadDocsTree(teamId); const collection = collections.find((item) => item.slug === sectionSlug); if (!collection) notFound(); + if (collection.api) redirect(collection.api.href); return ( diff --git a/apps/sites/app/sites/[teamId]/openapi.json/route.ts b/apps/sites/app/sites/[teamId]/openapi.json/route.ts new file mode 100644 index 0000000..95ea565 --- /dev/null +++ b/apps/sites/app/sites/[teamId]/openapi.json/route.ts @@ -0,0 +1,24 @@ +import { getApiSpecResponse } from "@/lib/api"; + +/** The team's OpenAPI spec as published with its API reference + * (docs/openapi-support.md). Open to any origin so tools can fetch it. */ +export async function GET( + request: Request, + { params }: { params: Promise<{ teamId: string }> }, +) { + const { teamId } = await params; + const upstream = await getApiSpecResponse( + teamId, + request.headers.get("if-none-match"), + ); + const headers = new Headers({ + "access-control-allow-origin": "*", + "cache-control": "public, max-age=0, must-revalidate", + }); + const etag = upstream.headers.get("etag"); + if (etag) headers.set("etag", etag); + if (upstream.status === 304) return new Response(null, { status: 304, headers }); + if (!upstream.ok) return new Response("Not found", { status: 404, headers }); + headers.set("content-type", "application/json; charset=utf-8"); + return new Response(await upstream.text(), { status: 200, headers }); +} diff --git a/apps/sites/components/api-reference/api-reference.tsx b/apps/sites/components/api-reference/api-reference.tsx new file mode 100644 index 0000000..12c82d2 --- /dev/null +++ b/apps/sites/components/api-reference/api-reference.tsx @@ -0,0 +1,532 @@ +import Link from "next/link"; +import { Download } from "lucide-react"; +import { TextRenderer } from "@frontlit/text-editor/renderer"; +import type { TextEditorContent } from "@frontlit/text-editor"; +import type { + ApiOperationSlice, + ApiParameter, + ApiRichText, + ApiSecurityScheme, + ApiServer, +} from "@frontlit/api-contract"; +import type { ThemeStyle } from "@frontlit/page-builder/models"; +import { + Badge, + Button, + Caption, + CodeSnippet, + Collapsible, + Header1, + Header3, + Header4, + InlineCode, + PageCard, + PageCardContent, + Section, + Table, + Tabs, + Text1, + Text2, +} from "@frontlit/page-builder/primitives/server"; +import { getRequestThemeStyle } from "@/lib/request-theme"; +import { API_REFERENCE_LABEL, type DocsCollection } from "@/lib/docs-tree"; +import type { PublicApiReference } from "@/lib/api"; +import { DocsSearch } from "@/components/docs-search"; +import { Breadcrumb, DocsSidebar } from "@/components/themed-docs"; +import { MethodBadge, StatusBadge } from "./method-badge"; +import { SchemaView, typeLabel } from "./schema-view"; +import { TryIt } from "./try-it"; + +function RichText({ value, theme }: { value?: ApiRichText; theme: ThemeStyle }) { + if (!value) return null; + if (value.kind === "doc") + return ( + + ); + return ( + + {value.text} + + ); +} + +function schemeLabel(scheme: ApiSecurityScheme, theme: ThemeStyle) { + if (scheme.type === "apiKey") + return ( + <> + API key in the{" "} + {scheme.name ?? scheme.key}{" "} + {scheme.in === "query" + ? "query parameter" + : scheme.in === "cookie" + ? "cookie" + : "header"} + + ); + if (scheme.type === "http" && scheme.scheme === "bearer") + return <>Bearer token{scheme.bearerFormat ? ` (${scheme.bearerFormat})` : ""}; + if (scheme.type === "http" && scheme.scheme === "basic") + return <>HTTP basic authentication; + if (scheme.type === "oauth2") return <>OAuth 2.0 access token; + if (scheme.type === "openIdConnect") return <>OpenID Connect token; + return <>{scheme.key}; +} + +/** Shell shared by the landing and operation pages: breadcrumb, search, the + * docs sidebar and a content area (plus an optional right column). */ +async function Shell({ + crumbs, + collections, + currentSlug, + teamId, + publicApiBaseUrl, + aside, + children, +}: { + crumbs: { label: string; href?: string }[]; + collections: DocsCollection[]; + currentSlug: string; + teamId: string; + publicApiBaseUrl: string; + aside?: React.ReactNode; + children: React.ReactNode; +}) { + const theme = await getRequestThemeStyle(); + return ( +
+ {/* Columns follow the theme's container width, not the viewport: + narrow themes (Editorial) drop the code column below the content. */} +
+
+ +
+ +
+
+
+ +
+
+ + + Browse articles + + +
+ +
+
+ {children} +
+ {aside ? ( + + ) : null} +
+
+
+ ); +} + +function ServerList({ servers, theme }: { servers: ApiServer[]; theme: ThemeStyle }) { + if (!servers.length) + return ( + + No server URL in the spec. + + ); + return ( +
    + {servers.map((server) => ( +
  • + + {server.url.replace(/\{([^}]+)\}/g, (match, name: string) => { + const variable = server.variables.find((v) => v.name === name); + return variable?.default || match; + })} + + {server.description ? ( + {server.description} + ) : null} +
  • + ))} +
+ ); +} + +export async function ThemedApiLanding({ + reference, + collections, + teamId, + publicApiBaseUrl, +}: { + reference: PublicApiReference; + collections: DocsCollection[]; + teamId: string; + publicApiBaseUrl: string; +}) { + const theme = await getRequestThemeStyle(); + const { info } = reference; + return ( + +
+
+ {API_REFERENCE_LABEL} + + + {info.title} + + {info.version ? ( + + v{info.version.replace(/^v/i, "")} + + ) : null} + +
+ + +
+
+ + + Base URL + + + + + + Authentication + {info.securitySchemes.length ? ( +
    + {info.securitySchemes.map((scheme) => ( +
  • + {schemeLabel(scheme, theme)} +
  • + ))} +
+ ) : ( + No authentication described. + )} +
+
+
+ {reference.groups.map((group) => ( +
+ {group.name} +
    + {group.operations.map((operation) => ( +
  • + + + + + + + {operation.path} + + + + {operation.title} + + + + +
  • + ))} +
+
+ ))} +
+ ); +} + +function ParameterTable({ + parameters, + operation, + theme, +}: { + parameters: ApiParameter[]; + operation: ApiOperationSlice; + theme: ThemeStyle; +}) { + return ( + ({ + name: ( + + {parameter.name} + {parameter.required ? : null} + {parameter.deprecated ? ( + + deprecated + + ) : null} + + ), + type: ( + + {parameter.schema + ? typeLabel(operation.schemas, parameter.schema) + : "string"} + + ), + description: parameter.description ? ( + + {parameter.description} + + ) : ( + + ), + }))} + /> + ); +} + +export async function ThemedApiOperation({ + reference, + operation, + route, + collections, + teamId, + publicApiBaseUrl, +}: { + reference: PublicApiReference; + operation: ApiOperationSlice; + route: string; + collections: DocsCollection[]; + teamId: string; + publicApiBaseUrl: string; +}) { + const theme = await getRequestThemeStyle(); + const locations = (["path", "query", "header", "cookie"] as const) + .map((location) => ({ + location, + parameters: operation.parameters.filter((p) => p.in === location), + })) + .filter((group) => group.parameters.length); + const examples = operation.responses.flatMap((response) => + response.content[0]?.example + ? [{ status: response.status, example: response.content[0].example }] + : [], + ); + const firstSuccess = Math.max( + 0, + operation.responses.findIndex((r) => /^2/.test(r.status)), + ); + const tryIt = ; + const aside = ( + <> +
{tryIt}
+
+ Request + ({ + label: sample.label, + content: , + }))} + /> +
+ {examples.length ? ( +
+ Response + ({ + label: example.status, + content: ( + + ), + }))} + /> +
+ ) : null} + + ); + + return ( + +
+ + + + {operation.path} + + {operation.deprecated ? ( + + Deprecated + + ) : null} + + {operation.title} + +
{tryIt}
+
+ +
+ Authentication + {operation.security.length ? ( +
    + {operation.security.map((requirement, index) => ( +
  • + + {index > 0 ? "Or " : ""} + {requirement.map((key, i) => { + const scheme = operation.securitySchemes.find((s) => s.key === key); + return ( + + {i > 0 ? " and " : ""} + {scheme ? schemeLabel(scheme, theme) : key} + + ); + })} + +
  • + ))} +
+ ) : ( + No authentication required. + )} +
+ + {locations.length ? ( +
+ Parameters + {locations.map((group) => ( +
+ + {group.location} parameters + + +
+ ))} +
+ ) : null} + + {operation.requestBody ? ( +
+ + Request body + {operation.requestBody.content.map((content) => ( + + {content.contentType} + + ))} + {operation.requestBody.required ? ( +
+ ) : null} + + {operation.requestBody.description ? ( + {operation.requestBody.description} + ) : null} + {operation.requestBody.content[0]?.schema ? ( + + ) : null} + + ) : null} + +
+ Responses + {operation.responses.map((response, index) => ( + + + + + + {response.description ?? ""} + + + } + > + {response.content[0]?.schema ? ( + + ) : ( +
+ )} + + ))} + + + ); +} diff --git a/apps/sites/components/api-reference/method-badge.tsx b/apps/sites/components/api-reference/method-badge.tsx new file mode 100644 index 0000000..9e4b081 --- /dev/null +++ b/apps/sites/components/api-reference/method-badge.tsx @@ -0,0 +1,61 @@ +import { Badge } from "@frontlit/page-builder/primitives/server"; +import type { ThemeStyle } from "@frontlit/page-builder/models"; + +type Variant = "default" | "secondary" | "destructive" | "outline"; + +/** Theme tokens only (docs/openapi-support.md §6.4 "Theme"). */ +function methodVariant(method: string): Variant { + switch (method.toLowerCase()) { + case "get": + return "secondary"; + case "post": + return "default"; + case "delete": + return "destructive"; + default: + return "outline"; + } +} + +function statusVariant(status: string): Variant { + // Primary, not secondary: some themes (Editorial) use a red-orange + // secondary that reads like the destructive 5xx badge. + if (/^2/.test(status)) return "default"; + if (/^5/.test(status)) return "destructive"; + return "outline"; +} + +const LABELS: Record = { delete: "DEL", options: "OPT" }; + +export function MethodBadge({ + method, + theme, + compact = false, +}: { + method: string; + theme: ThemeStyle; + compact?: boolean; +}) { + const lower = method.toLowerCase(); + return ( + + {compact ? (LABELS[lower] ?? lower.toUpperCase()) : lower.toUpperCase()} + + ); +} + +export function StatusBadge({ status, theme }: { status: string; theme: ThemeStyle }) { + return ( + + {status} + + ); +} diff --git a/apps/sites/components/api-reference/playground.tsx b/apps/sites/components/api-reference/playground.tsx new file mode 100644 index 0000000..de8692d --- /dev/null +++ b/apps/sites/components/api-reference/playground.tsx @@ -0,0 +1,580 @@ +"use client"; + +import { useEffect, useMemo, useRef, useState } from "react"; +import { X } from "lucide-react"; +import type { + ApiPlaygroundAuth, + ApiPlaygroundField, + ApiPlaygroundModel, +} from "@frontlit/api-contract"; +import { + Badge, + Button, + Caption, + CodeSnippet, + Collapsible, + Header4, + InlineCode, + Input, + Label, + NativeSelect, + PageCard, + Text2, + Textarea, +} from "@frontlit/page-builder/primitives/server"; +import type { ThemeStyle } from "@frontlit/page-builder/models"; +import { MethodBadge, StatusBadge } from "./method-badge"; +import { + buildRequest, + explainFailure, + missingInputs, + paramKey, + resolveServer, + sendRequest, + type PlaygroundInput, + type PlaygroundResult, +} from "./request"; + +/** Credentials and the chosen server live for the tab, shared across + * operation pages; never written to storage or sent to FrontLit. */ +const session: { + credentials: PlaygroundInput["credentials"]; + servers: Record }>; +} = { credentials: {}, servers: {} }; + +function Field({ + id, + label, + required, + hint, + theme, + children, +}: { + id: string; + label: string; + required?: boolean; + hint?: string; + theme: ThemeStyle; + children: React.ReactNode; +}) { + return ( +
+
: null} + + {children} + {hint ? ( + + ) : null} + + ); +} + +function ValueInput({ + id, + field, + value, + onChange, + theme, +}: { + id: string; + field: ApiPlaygroundField; + value: string; + onChange: (value: string) => void; + theme: ThemeStyle; +}) { + if (field.enum?.length) + return ( + onChange(e.target.value)} + > + + {field.enum.map((option) => ( + + ))} + + ); + return ( + onChange(e.target.value)} + /> + ); +} + +function AuthFields({ + schemes, + input, + setCredential, + theme, +}: { + schemes: ApiPlaygroundAuth[]; + input: PlaygroundInput; + setCredential: (key: string, patch: Record) => void; + theme: ThemeStyle; +}) { + return ( + <> + {schemes.map((scheme) => { + const credential = input.credentials[scheme.key] ?? {}; + const id = `auth-${scheme.key}`; + if (scheme.kind === "unsupported") + return ( + + ); + if (scheme.kind === "basic") + return ( +
+ + + setCredential(scheme.key, { username: e.target.value }) + } + /> + + + + setCredential(scheme.key, { password: e.target.value }) + } + /> + +
+ ); + return ( + + setCredential(scheme.key, { value: e.target.value })} + /> + + ); + })} + + ); +} + +export function Playground({ + model, + title, + theme, + onClose, +}: { + model: ApiPlaygroundModel; + title: string; + theme: ThemeStyle; + onClose: () => void; +}) { + const dialog = useRef(null); + const serverKey = model.servers.map((s) => s.url).join("|"); + const remembered = session.servers[serverKey]; + const [input, setInput] = useState(() => ({ + serverIndex: remembered?.index ?? 0, + serverVariables: remembered?.variables ?? {}, + parameters: {}, + body: model.body?.example ?? "", + fields: {}, + files: {}, + authOption: 0, + credentials: session.credentials, + })); + const [state, setState] = useState< + | { kind: "idle" } + | { kind: "sending" } + | { kind: "done"; result: PlaygroundResult } + | { kind: "error"; message: string } + >({ kind: "idle" }); + + // No cleanup: closing the dialog fires `close`, which would call + // `onClose`; unmounting removes the element and its modal state anyway. + useEffect(() => { + const element = dialog.current; + if (element && !element.open) element.showModal(); + }, []); + + const server = model.servers[input.serverIndex] ?? model.servers[0]; + const baseUrl = useMemo( + () => resolveServer(server.url, server.variables, input.serverVariables), + [server, input.serverVariables], + ); + const update = (patch: Partial) => + setInput((prev) => ({ ...prev, ...patch })); + const setServer = (index: number, variables: Record) => { + session.servers[serverKey] = { index, variables }; + update({ serverIndex: index, serverVariables: variables }); + }; + const setCredential = (key: string, patch: Record) => { + session.credentials = { + ...session.credentials, + [key]: { ...session.credentials[key], ...patch }, + }; + update({ credentials: session.credentials }); + }; + + const send = async () => { + const missing = missingInputs(model, input); + if (missing.length) { + setState({ kind: "error", message: `Fill in ${missing.join(", ")} first.` }); + return; + } + const request = buildRequest(model, input); + setState({ kind: "sending" }); + try { + setState({ kind: "done", result: await sendRequest(request) }); + } catch { + setState({ + kind: "error", + message: explainFailure( + request, + window.location.origin, + window.location.protocol, + ), + }); + } + }; + + const groups = (["path", "query", "header"] as const) + .map((location) => ({ + location, + parameters: model.parameters.filter((p) => p.in === location), + })) + .filter((group) => group.parameters.length); + const authOptions = model.auth; + const schemes = authOptions[input.authOption] ?? []; + + return ( + + +
+
+ {title} + + + + {model.path} + + +
+ +
+ +
{ + event.preventDefault(); + void send(); + }} + > +
+ {model.servers.length > 1 ? ( + + setServer(Number(e.target.value), {})} + > + {model.servers.map((s, index) => ( + + ))} + + + ) : null} + {server.variables.map((variable) => { + const id = `pg-var-${variable.name}`; + const value = input.serverVariables[variable.name] ?? ""; + const change = (next: string) => + setServer(input.serverIndex, { + ...input.serverVariables, + [variable.name]: next, + }); + return ( + + {variable.enum?.length ? ( + change(e.target.value)} + > + {variable.enum.map((option) => ( + + ))} + + ) : ( + change(e.target.value)} + /> + )} + + ); + })} +
+ + + {authOptions.length ? ( +
+ + Authentication + + {authOptions.length > 1 ? ( + update({ authOption: Number(e.target.value) })} + > + {authOptions.map((option, index) => ( + + ))} + + ) : null} + +
+ + ) : null} + + {groups.map((group) => ( +
+ + {group.location} parameters + + {group.parameters.map((parameter) => { + const key = paramKey(parameter.in, parameter.name); + const id = `pg-${key}`; + return ( + + + update({ parameters: { ...input.parameters, [key]: value } }) + } + theme={theme} + /> + + ); + })} +
+ ))} + + {model.body ? ( +
+ + + Body + + + {model.body.mediaType} + + + {model.body.kind === "json" || model.body.kind === "text" ? ( +
required—requiredNo response body.required + {hint} + + {scheme.key}: {scheme.reason} + + Requests go from your browser straight to{" "} + {baseUrl}. + + Credentials stay in this browser tab and are never sent to this site. +