From 93ea237a3fdf6454e99cda4755654218ab8c62b4 Mon Sep 17 00:00:00 2001 From: MisterJimson Date: Tue, 21 Jul 2026 15:25:05 -0400 Subject: [PATCH 1/3] Support OAuth client_secret_basic --- .changeset/oauth-client-basic-auth.md | 5 + apps/cloud/drizzle/0012_useful_jigsaw.sql | 1 + apps/cloud/drizzle/meta/0012_snapshot.json | 1302 +++++++++++++++++ apps/cloud/drizzle/meta/_journal.json | 7 + apps/cloud/src/db/executor-schema.ts | 1 + .../drizzle/0005_chemical_black_widow.sql | 1 + apps/local/drizzle/meta/0005_snapshot.json | 934 ++++++++++++ apps/local/drizzle/meta/_journal.json | 7 + apps/local/src/db/executor-schema.ts | 1 + apps/local/src/db/sqlite-fumadb.ts | 6 + packages/core/api/src/handlers/oauth.ts | 1 + packages/core/api/src/oauth/api.ts | 5 + packages/core/sdk/src/core-schema.ts | 3 + packages/core/sdk/src/executor.ts | 9 + packages/core/sdk/src/oauth-client.ts | 16 + packages/core/sdk/src/oauth-flow.test.ts | 192 +++ packages/core/sdk/src/oauth-helpers.ts | 8 + .../core/sdk/src/oauth-list-clients.test.ts | 37 + packages/core/sdk/src/oauth-service.ts | 15 + packages/core/sdk/src/shared.ts | 1 + .../core/sdk/src/testing/oauth-test-server.ts | 57 +- packages/react/src/api/atoms.tsx | 4 + .../src/components/add-account-modal.tsx | 3 + .../src/components/oauth-client-form.tsx | 57 + 24 files changed, 2670 insertions(+), 3 deletions(-) create mode 100644 .changeset/oauth-client-basic-auth.md create mode 100644 apps/cloud/drizzle/0012_useful_jigsaw.sql create mode 100644 apps/cloud/drizzle/meta/0012_snapshot.json create mode 100644 apps/local/drizzle/0005_chemical_black_widow.sql create mode 100644 apps/local/drizzle/meta/0005_snapshot.json diff --git a/.changeset/oauth-client-basic-auth.md b/.changeset/oauth-client-basic-auth.md new file mode 100644 index 000000000..84dd64472 --- /dev/null +++ b/.changeset/oauth-client-basic-auth.md @@ -0,0 +1,5 @@ +--- +"@executor-js/sdk": patch +--- + +Allow registered OAuth clients to use `client_secret_basic` at the token endpoint. The selected client-auth method is persisted and reused for authorization-code exchanges, client-credentials mints, token refreshes, and client-credentials re-mints; existing clients continue to default to `client_secret_post`. diff --git a/apps/cloud/drizzle/0012_useful_jigsaw.sql b/apps/cloud/drizzle/0012_useful_jigsaw.sql new file mode 100644 index 000000000..cd7f5b3d8 --- /dev/null +++ b/apps/cloud/drizzle/0012_useful_jigsaw.sql @@ -0,0 +1 @@ +ALTER TABLE "oauth_client" ADD COLUMN "token_endpoint_auth_method" text; diff --git a/apps/cloud/drizzle/meta/0012_snapshot.json b/apps/cloud/drizzle/meta/0012_snapshot.json new file mode 100644 index 000000000..b052a77c1 --- /dev/null +++ b/apps/cloud/drizzle/meta/0012_snapshot.json @@ -0,0 +1,1302 @@ +{ + "id": "bc70a2f9-acb8-4579-8ba6-e26caac49801", + "prevId": "e285be7d-2427-4b1b-8b86-50a80ada25af", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.accounts": { + "name": "accounts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.memberships": { + "name": "memberships", + "schema": "", + "columns": { + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "memberships_account_id_accounts_id_fk": { + "name": "memberships_account_id_accounts_id_fk", + "tableFrom": "memberships", + "tableTo": "accounts", + "columnsFrom": ["account_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "memberships_organization_id_organizations_id_fk": { + "name": "memberships_organization_id_organizations_id_fk", + "tableFrom": "memberships", + "tableTo": "organizations", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "memberships_account_id_organization_id_pk": { + "name": "memberships_account_id_organization_id_pk", + "columns": ["account_id", "organization_id"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organizations": { + "name": "organizations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "organizations_slug_unique": { + "name": "organizations_slug_unique", + "columns": [ + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.blob": { + "name": "blob", + "schema": "", + "columns": { + "namespace": { + "name": "namespace", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "row_id": { + "name": "row_id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "blob_id_uidx": { + "name": "blob_id_uidx", + "columns": [ + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.connection": { + "name": "connection", + "schema": "", + "columns": { + "integration": { + "name": "integration", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "template": { + "name": "template", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "item_ids": { + "name": "item_ids", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "identity_label": { + "name": "identity_label", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_health": { + "name": "last_health", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "tools_synced_at": { + "name": "tools_synced_at", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "oauth_client": { + "name": "oauth_client", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "oauth_client_owner": { + "name": "oauth_client_owner", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_item_id": { + "name": "refresh_item_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "oauth_scope": { + "name": "oauth_scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "oauth_token_url": { + "name": "oauth_token_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_state": { + "name": "provider_state", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "row_id": { + "name": "row_id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "tenant": { + "name": "tenant", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "owner": { + "name": "owner", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "subject": { + "name": "subject", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "connection_uidx": { + "name": "connection_uidx", + "columns": [ + { + "expression": "tenant", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "owner", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "subject", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "integration", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.definition": { + "name": "definition", + "schema": "", + "columns": { + "integration": { + "name": "integration", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "connection": { + "name": "connection", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "schema": { + "name": "schema", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "row_id": { + "name": "row_id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "tenant": { + "name": "tenant", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "owner": { + "name": "owner", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "subject": { + "name": "subject", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "definition_uidx": { + "name": "definition_uidx", + "columns": [ + { + "expression": "tenant", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "owner", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "subject", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "integration", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "connection", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.integration": { + "name": "integration", + "schema": "", + "columns": { + "slug": { + "name": "slug", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "config": { + "name": "config", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "health_check": { + "name": "health_check", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "config_revised_at": { + "name": "config_revised_at", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "can_remove": { + "name": "can_remove", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "can_refresh": { + "name": "can_refresh", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "row_id": { + "name": "row_id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "tenant": { + "name": "tenant", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "integration_uidx": { + "name": "integration_uidx", + "columns": [ + { + "expression": "tenant", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.oauth_client": { + "name": "oauth_client", + "schema": "", + "columns": { + "slug": { + "name": "slug", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "authorization_url": { + "name": "authorization_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token_url": { + "name": "token_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "grant": { + "name": "grant", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_secret_item_id": { + "name": "client_secret_item_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resource": { + "name": "resource", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "token_endpoint_auth_method": { + "name": "token_endpoint_auth_method", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "origin_kind": { + "name": "origin_kind", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "origin_integration": { + "name": "origin_integration", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "origin_issuer": { + "name": "origin_issuer", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "origin_redirect_uri": { + "name": "origin_redirect_uri", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "row_id": { + "name": "row_id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "tenant": { + "name": "tenant", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "owner": { + "name": "owner", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "subject": { + "name": "subject", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "oauth_client_uidx": { + "name": "oauth_client_uidx", + "columns": [ + { + "expression": "tenant", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "owner", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "subject", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.oauth_session": { + "name": "oauth_session", + "schema": "", + "columns": { + "state": { + "name": "state", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "client_slug": { + "name": "client_slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "integration": { + "name": "integration", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "template": { + "name": "template", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "redirect_url": { + "name": "redirect_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pkce_verifier": { + "name": "pkce_verifier", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "identity_label": { + "name": "identity_label", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "payload": { + "name": "payload", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "row_id": { + "name": "row_id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "tenant": { + "name": "tenant", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "owner": { + "name": "owner", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "subject": { + "name": "subject", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "oauth_session_uidx": { + "name": "oauth_session_uidx", + "columns": [ + { + "expression": "tenant", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "state", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugin_storage": { + "name": "plugin_storage", + "schema": "", + "columns": { + "plugin_id": { + "name": "plugin_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "collection": { + "name": "collection", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "data": { + "name": "data", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "row_id": { + "name": "row_id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "tenant": { + "name": "tenant", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "owner": { + "name": "owner", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "subject": { + "name": "subject", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "plugin_storage_uidx": { + "name": "plugin_storage_uidx", + "columns": [ + { + "expression": "tenant", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "owner", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "subject", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "collection", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.private_executor_cloud_settings": { + "name": "private_executor_cloud_settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "version": { + "name": "version", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "'1.0.0'" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tool": { + "name": "tool", + "schema": "", + "columns": { + "integration": { + "name": "integration", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "connection": { + "name": "connection", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "input_schema": { + "name": "input_schema", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "output_schema": { + "name": "output_schema", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "annotations": { + "name": "annotations", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "row_id": { + "name": "row_id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "tenant": { + "name": "tenant", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "owner": { + "name": "owner", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "subject": { + "name": "subject", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "tool_uidx": { + "name": "tool_uidx", + "columns": [ + { + "expression": "tenant", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "owner", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "subject", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "integration", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "connection", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tool_policy": { + "name": "tool_policy", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "pattern": { + "name": "pattern", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "position": { + "name": "position", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "row_id": { + "name": "row_id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "tenant": { + "name": "tenant", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "owner": { + "name": "owner", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "subject": { + "name": "subject", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "tool_policy_uidx": { + "name": "tool_policy_uidx", + "columns": [ + { + "expression": "tenant", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "owner", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "subject", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} diff --git a/apps/cloud/drizzle/meta/_journal.json b/apps/cloud/drizzle/meta/_journal.json index 72f9cbb97..1bdddfafc 100644 --- a/apps/cloud/drizzle/meta/_journal.json +++ b/apps/cloud/drizzle/meta/_journal.json @@ -85,6 +85,13 @@ "when": 1784581232732, "tag": "0011_tense_caretaker", "breakpoints": true + }, + { + "idx": 12, + "version": "7", + "when": 1784661475789, + "tag": "0012_useful_jigsaw", + "breakpoints": true } ] } diff --git a/apps/cloud/src/db/executor-schema.ts b/apps/cloud/src/db/executor-schema.ts index 350b0c18b..71dcae47d 100644 --- a/apps/cloud/src/db/executor-schema.ts +++ b/apps/cloud/src/db/executor-schema.ts @@ -83,6 +83,7 @@ export const oauth_client = pgTable( client_id: text("client_id").notNull(), client_secret_item_id: text("client_secret_item_id"), resource: text("resource"), + token_endpoint_auth_method: text("token_endpoint_auth_method"), origin_kind: text("origin_kind"), origin_integration: text("origin_integration"), origin_issuer: text("origin_issuer"), diff --git a/apps/local/drizzle/0005_chemical_black_widow.sql b/apps/local/drizzle/0005_chemical_black_widow.sql new file mode 100644 index 000000000..2abd95772 --- /dev/null +++ b/apps/local/drizzle/0005_chemical_black_widow.sql @@ -0,0 +1 @@ +ALTER TABLE `oauth_client` ADD `token_endpoint_auth_method` text; \ No newline at end of file diff --git a/apps/local/drizzle/meta/0005_snapshot.json b/apps/local/drizzle/meta/0005_snapshot.json new file mode 100644 index 000000000..c48134beb --- /dev/null +++ b/apps/local/drizzle/meta/0005_snapshot.json @@ -0,0 +1,934 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "3f8bf067-80ea-4545-be0c-e0fc20919d45", + "prevId": "85f30981-cda9-45df-b757-b2836fb70e07", + "tables": { + "blob": { + "name": "blob", + "columns": { + "namespace": { + "name": "namespace", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "row_id": { + "name": "row_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "blob_id_uidx": { + "name": "blob_id_uidx", + "columns": ["id"], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "connection": { + "name": "connection", + "columns": { + "integration": { + "name": "integration", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "template": { + "name": "template", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "item_ids": { + "name": "item_ids", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "identity_label": { + "name": "identity_label", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "oauth_client": { + "name": "oauth_client", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "oauth_client_owner": { + "name": "oauth_client_owner", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "refresh_item_id": { + "name": "refresh_item_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "blob", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "oauth_scope": { + "name": "oauth_scope", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "oauth_token_url": { + "name": "oauth_token_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "provider_state": { + "name": "provider_state", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "row_id": { + "name": "row_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant": { + "name": "tenant", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "owner": { + "name": "owner", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject": { + "name": "subject", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "connection_uidx": { + "name": "connection_uidx", + "columns": ["tenant", "owner", "subject", "integration", "name"], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "definition": { + "name": "definition", + "columns": { + "integration": { + "name": "integration", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "connection": { + "name": "connection", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "plugin_id": { + "name": "plugin_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "schema": { + "name": "schema", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "row_id": { + "name": "row_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant": { + "name": "tenant", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "owner": { + "name": "owner", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject": { + "name": "subject", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "definition_uidx": { + "name": "definition_uidx", + "columns": ["tenant", "owner", "subject", "integration", "connection", "name"], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "integration": { + "name": "integration", + "columns": { + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "plugin_id": { + "name": "plugin_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "can_remove": { + "name": "can_remove", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "can_refresh": { + "name": "can_refresh", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "row_id": { + "name": "row_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant": { + "name": "tenant", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "integration_uidx": { + "name": "integration_uidx", + "columns": ["tenant", "slug"], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "oauth_client": { + "name": "oauth_client", + "columns": { + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "authorization_url": { + "name": "authorization_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_url": { + "name": "token_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "grant": { + "name": "grant", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_secret_item_id": { + "name": "client_secret_item_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "resource": { + "name": "resource", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "token_endpoint_auth_method": { + "name": "token_endpoint_auth_method", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "origin_kind": { + "name": "origin_kind", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "origin_integration": { + "name": "origin_integration", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "origin_issuer": { + "name": "origin_issuer", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "origin_redirect_uri": { + "name": "origin_redirect_uri", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "row_id": { + "name": "row_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant": { + "name": "tenant", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "owner": { + "name": "owner", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject": { + "name": "subject", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "oauth_client_uidx": { + "name": "oauth_client_uidx", + "columns": ["tenant", "owner", "subject", "slug"], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "oauth_session": { + "name": "oauth_session", + "columns": { + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_slug": { + "name": "client_slug", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "integration": { + "name": "integration", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "template": { + "name": "template", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "redirect_url": { + "name": "redirect_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "pkce_verifier": { + "name": "pkce_verifier", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "identity_label": { + "name": "identity_label", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "payload": { + "name": "payload", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "blob", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "row_id": { + "name": "row_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant": { + "name": "tenant", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "owner": { + "name": "owner", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject": { + "name": "subject", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "oauth_session_uidx": { + "name": "oauth_session_uidx", + "columns": ["tenant", "state"], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "plugin_storage": { + "name": "plugin_storage", + "columns": { + "plugin_id": { + "name": "plugin_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "collection": { + "name": "collection", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "row_id": { + "name": "row_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant": { + "name": "tenant", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "owner": { + "name": "owner", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject": { + "name": "subject", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "plugin_storage_uidx": { + "name": "plugin_storage_uidx", + "columns": ["tenant", "owner", "subject", "plugin_id", "collection", "key"], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "tool": { + "name": "tool", + "columns": { + "integration": { + "name": "integration", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "connection": { + "name": "connection", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "plugin_id": { + "name": "plugin_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "input_schema": { + "name": "input_schema", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "output_schema": { + "name": "output_schema", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "annotations": { + "name": "annotations", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "row_id": { + "name": "row_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant": { + "name": "tenant", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "owner": { + "name": "owner", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject": { + "name": "subject", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "tool_uidx": { + "name": "tool_uidx", + "columns": ["tenant", "owner", "subject", "integration", "connection", "name"], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "tool_policy": { + "name": "tool_policy", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "pattern": { + "name": "pattern", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "position": { + "name": "position", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "row_id": { + "name": "row_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "tenant": { + "name": "tenant", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "owner": { + "name": "owner", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject": { + "name": "subject", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "tool_policy_uidx": { + "name": "tool_policy_uidx", + "columns": ["tenant", "owner", "subject", "id"], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} diff --git a/apps/local/drizzle/meta/_journal.json b/apps/local/drizzle/meta/_journal.json index 8b4b73e4a..a805b8a4e 100644 --- a/apps/local/drizzle/meta/_journal.json +++ b/apps/local/drizzle/meta/_journal.json @@ -36,6 +36,13 @@ "when": 1784581228503, "tag": "0004_cultured_shadowcat", "breakpoints": true + }, + { + "idx": 5, + "version": "6", + "when": 1784661475760, + "tag": "0005_chemical_black_widow", + "breakpoints": true } ] } diff --git a/apps/local/src/db/executor-schema.ts b/apps/local/src/db/executor-schema.ts index baa418030..dc86cde31 100644 --- a/apps/local/src/db/executor-schema.ts +++ b/apps/local/src/db/executor-schema.ts @@ -73,6 +73,7 @@ export const oauth_client = sqliteTable( client_id: text("client_id").notNull(), client_secret_item_id: text("client_secret_item_id"), resource: text("resource"), + token_endpoint_auth_method: text("token_endpoint_auth_method"), origin_kind: text("origin_kind"), origin_integration: text("origin_integration"), origin_issuer: text("origin_issuer"), diff --git a/apps/local/src/db/sqlite-fumadb.ts b/apps/local/src/db/sqlite-fumadb.ts index 7ffb8128a..11c7462a4 100644 --- a/apps/local/src/db/sqlite-fumadb.ts +++ b/apps/local/src/db/sqlite-fumadb.ts @@ -105,6 +105,12 @@ export const createSqliteFumaDb = async ( ) { await client.execute("ALTER TABLE oauth_client ADD COLUMN origin_redirect_uri TEXT"); } + if ( + oauthClientColumns.rows.length > 0 && + !oauthClientColumns.rows.some((column) => column["name"] === "token_endpoint_auth_method") + ) { + await client.execute("ALTER TABLE oauth_client ADD COLUMN token_endpoint_auth_method TEXT"); + } const { db, fuma } = createExecutorFumaDb(drizzleDb, { tables: options.tables, diff --git a/packages/core/api/src/handlers/oauth.ts b/packages/core/api/src/handlers/oauth.ts index 5187908c9..91a2f699b 100644 --- a/packages/core/api/src/handlers/oauth.ts +++ b/packages/core/api/src/handlers/oauth.ts @@ -105,6 +105,7 @@ export const OAuthHandlers = HttpApiBuilder.group(ExecutorApi, "oauth", (handler clientSecret: payload.clientSecret, resource: payload.resource ?? null, origin: { kind: "manual", integration: payload.originIntegration ?? null }, + tokenEndpointAuthMethod: payload.tokenEndpointAuthMethod, }); return { client }; }), diff --git a/packages/core/api/src/oauth/api.ts b/packages/core/api/src/oauth/api.ts index 4bf74786a..63d71b416 100644 --- a/packages/core/api/src/oauth/api.ts +++ b/packages/core/api/src/oauth/api.ts @@ -69,6 +69,9 @@ const CreateClientPayload = Schema.Struct({ /** Integration whose connect dialog registered this manual app. Recorded so * the picker matches it to this integration by intent, not root domain. */ originIntegration: Schema.optional(Schema.NullOr(IntegrationSlug)), + /** Token-endpoint client auth: "body" (client_secret_post, default) or + * "basic" (client_secret_basic). Omitted means "body". */ + tokenEndpointAuthMethod: Schema.optional(Schema.Literals(["body", "basic"])), }); const CreateClientResponse = Schema.Struct({ @@ -115,6 +118,8 @@ const OAuthClientSummaryResponse = Schema.Struct({ tokenUrl: Schema.String, resource: Schema.optional(Schema.NullOr(Schema.String)), clientId: Schema.String, + /** Token-endpoint client auth ("body" | "basic"); omitted means "body". */ + tokenEndpointAuthMethod: Schema.optional(Schema.Literals(["body", "basic"])), origin: Schema.Union([ Schema.Struct({ kind: Schema.Literal("manual") }), Schema.Struct({ diff --git a/packages/core/sdk/src/core-schema.ts b/packages/core/sdk/src/core-schema.ts index e472ed9bb..65c0e2c9c 100644 --- a/packages/core/sdk/src/core-schema.ts +++ b/packages/core/sdk/src/core-schema.ts @@ -219,6 +219,9 @@ export const coreTables = defineTables({ // re-minted access token stays bound to the same resource. Null when the // provider doesn't use resource indicators. resource: nullableTextColumn("resource"), + // Token-endpoint client authentication. Null preserves the historical + // client_secret_post default; "basic" selects client_secret_basic. + token_endpoint_auth_method: nullableTextColumn("token_endpoint_auth_method"), // Where this oauth_client came from. Null in old databases is treated as // "manual" by the service layer. origin_kind: nullableTextColumn("origin_kind"), diff --git a/packages/core/sdk/src/executor.ts b/packages/core/sdk/src/executor.ts index 42dee8cd1..fb7230027 100644 --- a/packages/core/sdk/src/executor.ts +++ b/packages/core/sdk/src/executor.ts @@ -146,6 +146,7 @@ import { collectReferencedDefinitions } from "./schema-refs"; import { refreshAccessToken, exchangeClientCredentials, + parseClientAuthMethod, shouldRefreshToken, type OAuthEndpointUrlPolicy, } from "./oauth-helpers"; @@ -1543,6 +1544,12 @@ export const createExecutor = { ), ); + it.effect( + "client_credentials with tokenEndpointAuthMethod=basic sends HTTP Basic to the token endpoint", + () => + Effect.scoped( + Effect.gen(function* () { + // The AS is configured to REJECT body (client_secret_post) auth, so a + // successful inline mint proves the executor actually sent HTTP Basic. + const server = yield* serveOAuthTestServer({ + scopes: ["read"], + requireClientAuthMethod: "basic", + }); + const { executor } = yield* makeTestWorkspaceHarness({ plugins, redirectUri: null }); + yield* executor.acme.seed(); + + yield* executor.oauth.createClient({ + owner: "org", + slug: CLIENT, + authorizationUrl: server.authorizationEndpoint, + tokenUrl: server.tokenEndpoint, + grant: "client_credentials", + clientId: "test-client", + clientSecret: "test-secret", + tokenEndpointAuthMethod: "basic", + }); + + const started = yield* executor.oauth.start({ + owner: "org", + client: CLIENT, + clientOwner: "org", + name: ConnectionName.make("cc"), + integration: INTEG, + template: TEMPLATE, + }); + expect(started.status).toBe("connected"); + // The single token request used HTTP Basic, not body params. + expect(yield* server.tokenRequestAuthMethods).toEqual(["basic"]); + }), + ), + ); + + it.effect("client_credentials without Basic is rejected by a Basic-only token endpoint", () => + Effect.scoped( + Effect.gen(function* () { + const server = yield* serveOAuthTestServer({ + scopes: ["read"], + requireClientAuthMethod: "basic", + }); + const { executor } = yield* makeTestWorkspaceHarness({ plugins, redirectUri: null }); + yield* executor.acme.seed(); + + // No tokenEndpointAuthMethod => the "body" (client_secret_post) default, + // which the Basic-only AS rejects. Proves the field actually changes the + // wire behavior rather than being inert. + yield* executor.oauth.createClient({ + owner: "org", + slug: CLIENT, + authorizationUrl: server.authorizationEndpoint, + tokenUrl: server.tokenEndpoint, + grant: "client_credentials", + clientId: "test-client", + clientSecret: "test-secret", + }); + + const error = yield* Effect.flip( + executor.oauth.start({ + owner: "org", + client: CLIENT, + clientOwner: "org", + name: ConnectionName.make("cc"), + integration: INTEG, + template: TEMPLATE, + }), + ); + expect(Predicate.isTagged("OAuthStartError")(error)).toBe(true); + expect(yield* server.tokenRequestAuthMethods).toEqual(["body"]); + }), + ), + ); + + it.effect( + "authorization_code with tokenEndpointAuthMethod=basic sends HTTP Basic on the code exchange", + () => + Effect.scoped( + Effect.gen(function* () { + // The Basic-only AS rejects body, so a successful code exchange proves + // clientAuth is threaded into the authorization_code complete path too. + const server = yield* serveOAuthTestServer({ + scopes: ["read"], + requireClientAuthMethod: "basic", + }); + const { executor } = yield* makeTestWorkspaceHarness({ + plugins, + redirectUri: "https://app.test/oauth/callback", + }); + yield* executor.acme.seed(); + + yield* executor.oauth.createClient({ + owner: "org", + slug: CLIENT, + authorizationUrl: server.authorizationEndpoint, + tokenUrl: server.tokenEndpoint, + grant: "authorization_code", + clientId: "test-client", + clientSecret: "test-secret", + tokenEndpointAuthMethod: "basic", + }); + + const started = yield* executor.oauth.start({ + owner: "org", + client: CLIENT, + clientOwner: "org", + name: ConnectionName.make("main"), + integration: INTEG, + template: TEMPLATE, + }); + expect(started.status).toBe("redirect"); + if (started.status !== "redirect") return; + const callback = yield* server.completeAuthorizationCodeFlow({ + authorizationUrl: started.authorizationUrl, + }); + const connection = yield* executor.oauth.complete({ + state: started.state, + code: callback.code, + }); + expect(connection.name).toBe("main"); + // Only the code exchange hit /token, and it used HTTP Basic. + expect(yield* server.tokenRequestAuthMethods).toEqual(["basic"]); + }), + ), + ); + it.effect("complete with an unknown state fails OAuthSessionNotFoundError", () => Effect.scoped( Effect.gen(function* () { @@ -638,6 +769,67 @@ describe("oauth.start / oauth.complete", () => { }); describe("oauth token refresh in resolveConnectionValue", () => { + it.effect("an expired client_credentials connection re-mints via HTTP Basic", () => + Effect.scoped( + Effect.gen(function* () { + // Basic-only AS: every token request (initial mint AND the expiry + // re-mint) must carry HTTP Basic or it 401s. + const server = yield* serveOAuthTestServer({ + scopes: ["read"], + requireClientAuthMethod: "basic", + }); + const harness = yield* makeTestWorkspaceHarness({ plugins, redirectUri: null }); + const { executor, config } = harness; + yield* executor.acme.seed(); + + yield* executor.oauth.createClient({ + owner: "org", + slug: CLIENT, + authorizationUrl: server.authorizationEndpoint, + tokenUrl: server.tokenEndpoint, + grant: "client_credentials", + clientId: "test-client", + clientSecret: "test-secret", + tokenEndpointAuthMethod: "basic", + }); + + const started = yield* executor.oauth.start({ + owner: "org", + client: CLIENT, + clientOwner: "org", + name: ConnectionName.make("cc"), + integration: INTEG, + template: TEMPLATE, + }); + expect(started.status).toBe("connected"); + + const firstToken = (yield* executor.execute( + ToolAddress.make("tools.acme.org.cc.whoami"), + {}, + )) as { token: string }; + expect(firstToken.token).toMatch(/^at_/); + + // client_credentials has no refresh token; expiring forces a fresh mint. + yield* Effect.promise(() => + config.db.updateMany("connection", { + where: (b) => b("name", "=", "cc"), + set: { expires_at: Date.now() - 60_000 }, + }), + ); + + const refreshedToken = (yield* executor.execute( + ToolAddress.make("tools.acme.org.cc.whoami"), + {}, + )) as { token: string }; + expect(refreshedToken.token).toMatch(/^at_/); + expect(refreshedToken.token).not.toBe(firstToken.token); + expect(yield* server.acceptsAccessToken(refreshedToken.token)).toBe(true); + // Both the initial mint and the re-mint authenticated via HTTP Basic. + expect(yield* server.tokenRequestAuthMethods).toEqual(["basic", "basic"]); + }), + ), + ); + it.effect("an expired access token is refreshed before resolving", () => Effect.scoped( Effect.gen(function* () { diff --git a/packages/core/sdk/src/oauth-helpers.ts b/packages/core/sdk/src/oauth-helpers.ts index daf5892cd..19cfa1144 100644 --- a/packages/core/sdk/src/oauth-helpers.ts +++ b/packages/core/sdk/src/oauth-helpers.ts @@ -384,6 +384,14 @@ export type ClientAuthMethod = "body" | "basic"; */ export const DEFAULT_CLIENT_AUTH_METHOD: ClientAuthMethod = "body"; +/** Resolve a stored/raw token-endpoint client-auth value to a {@link + * ClientAuthMethod}. Only `"basic"` is distinguished; everything else, + * including null (old `oauth_client` rows) and unknown strings, falls through + * to {@link DEFAULT_CLIENT_AUTH_METHOD} (`"body"`). Unlike the OAuth grant, + * an unrecognised value here is a benign default, not a corrupt-row error. */ +export const parseClientAuthMethod = (value: unknown): ClientAuthMethod => + value === "basic" ? "basic" : DEFAULT_CLIENT_AUTH_METHOD; + const asFromTokenUrl = ( tokenUrl: string, endpointUrlPolicy: OAuthEndpointUrlPolicy = {}, diff --git a/packages/core/sdk/src/oauth-list-clients.test.ts b/packages/core/sdk/src/oauth-list-clients.test.ts index 086513ce3..04d616645 100644 --- a/packages/core/sdk/src/oauth-list-clients.test.ts +++ b/packages/core/sdk/src/oauth-list-clients.test.ts @@ -108,6 +108,43 @@ describe("oauth.listClients", () => { ), ); + it.effect("round-trips tokenEndpointAuthMethod: only 'basic' is materialized", () => + Effect.scoped( + Effect.gen(function* () { + const { executor } = yield* makeTestWorkspaceHarness({ plugins }); + + yield* executor.oauth.createClient({ + owner: "org", + slug: ORG_CLIENT, + authorizationUrl: "", + tokenUrl: "https://acme.test/token", + grant: "client_credentials", + clientId: "basic-client", + clientSecret: "s3cret", + tokenEndpointAuthMethod: "basic", + }); + // A second client with the default (body) method, sent explicitly. + yield* executor.oauth.createClient({ + owner: "user", + slug: USER_CLIENT, + authorizationUrl: "", + tokenUrl: "https://byo.test/token", + grant: "client_credentials", + clientId: "body-client", + clientSecret: "s3cret", + tokenEndpointAuthMethod: "body", + }); + + const bySlug = new Map( + (yield* executor.oauth.listClients()).map((c) => [String(c.slug), c]), + ); + // "basic" surfaces on the summary; "body" stays implicit (omitted). + expect(bySlug.get(String(ORG_CLIENT))?.tokenEndpointAuthMethod).toBe("basic"); + expect(bySlug.get(String(USER_CLIENT))?.tokenEndpointAuthMethod).toBeUndefined(); + }), + ), + ); + it.effect("hides another user's clients from the caller", () => Effect.scoped( Effect.gen(function* () { diff --git a/packages/core/sdk/src/oauth-service.ts b/packages/core/sdk/src/oauth-service.ts index 65227a0f9..f39b22c92 100644 --- a/packages/core/sdk/src/oauth-service.ts +++ b/packages/core/sdk/src/oauth-service.ts @@ -66,7 +66,9 @@ import { exchangeAuthorizationCode, exchangeClientCredentials, isLoopbackHttpUrl, + parseClientAuthMethod, rebindTokenEndpointHostToCallbackDomain, + type ClientAuthMethod, type OAuth2TokenResponse, type OAuthEndpointUrlPolicy, } from "./oauth-helpers"; @@ -389,6 +391,8 @@ interface LoadedOAuthClient { /** Resolved literal secret (read from the provider via the stored item id). */ readonly clientSecret: string; readonly resource: string | null; + /** Resolved token-endpoint client auth method ("body" | "basic"). */ + readonly tokenEndpointAuthMethod: ClientAuthMethod; } /** Where an OAuth app's client secret is stored in the default writable @@ -632,6 +636,9 @@ export const makeOAuthService = (deps: OAuthServiceDeps): OAuthService => { client_id: input.clientId, client_secret_item_id: clientSecretItemIdValue, resource: input.resource ?? null, + // Persist only a non-default ("basic") method; "body"/undefined stays + // null, which parseClientAuthMethod resolves back to the "body" default. + token_endpoint_auth_method: input.tokenEndpointAuthMethod === "basic" ? "basic" : null, origin_kind: input.origin?.kind ?? "manual", // Recorded intent, kept for BOTH origins: a manual app registered from // an integration's dialog stamps its integration so the picker can @@ -965,6 +972,11 @@ export const makeOAuthService = (deps: OAuthServiceDeps): OAuthService => { tokenUrl: String(row.token_url), resource: row.resource == null ? null : String(row.resource), clientId: String(row.client_id), + // Only surface a non-default ("basic") method; "body" is implicit + // (undefined), matching how it is persisted and sent on create. + ...(parseClientAuthMethod(row.token_endpoint_auth_method) === "basic" + ? { tokenEndpointAuthMethod: "basic" as const } + : {}), origin: parseOAuthClientOrigin(row), } satisfies OAuthClientSummary); }), @@ -1021,6 +1033,7 @@ export const makeOAuthService = (deps: OAuthServiceDeps): OAuthService => { clientId: String(row.client_id), clientSecret, resource: row.resource == null ? null : String(row.resource), + tokenEndpointAuthMethod: parseClientAuthMethod(row.token_endpoint_auth_method), } satisfies LoadedOAuthClient; }); }), @@ -1095,6 +1108,7 @@ export const makeOAuthService = (deps: OAuthServiceDeps): OAuthService => { clientSecret: client.clientSecret, scopes: requestedScopes, resource: client.resource ?? undefined, + clientAuth: client.tokenEndpointAuthMethod, endpointUrlPolicy: deps.endpointUrlPolicy, fetch, }).pipe( @@ -1289,6 +1303,7 @@ export const makeOAuthService = (deps: OAuthServiceDeps): OAuthService => { codeVerifier: session.pkceVerifier, code: input.code, resource: client.resource ?? undefined, + clientAuth: client.tokenEndpointAuthMethod, endpointUrlPolicy: deps.endpointUrlPolicy, fetch, }).pipe( diff --git a/packages/core/sdk/src/shared.ts b/packages/core/sdk/src/shared.ts index e78b56577..8edb48b75 100644 --- a/packages/core/sdk/src/shared.ts +++ b/packages/core/sdk/src/shared.ts @@ -129,6 +129,7 @@ export { // OAuth wire contracts (data + tagged errors; the flow impl is server-only). export { type OAuthGrant, + type ClientAuthMethod, type OAuthAuthentication, type OAuthClient, type OAuthClientOrigin, diff --git a/packages/core/sdk/src/testing/oauth-test-server.ts b/packages/core/sdk/src/testing/oauth-test-server.ts index 17413f5f4..a31886054 100644 --- a/packages/core/sdk/src/testing/oauth-test-server.ts +++ b/packages/core/sdk/src/testing/oauth-test-server.ts @@ -66,6 +66,13 @@ export interface OAuthTestServerOptions { * `redirect_uris` entry is approved. Mirrors authorization servers (e.g. * Vercel) that only accept loopback redirect URIs for anonymous DCR. */ readonly approveRedirectUri?: (uri: string) => boolean; + /** When set, the `/token` endpoint ENFORCES a client-auth transport on every + * request. `"basic"` rejects (401 invalid_client) any request that does not + * carry HTTP Basic credentials (proves the client used `client_secret_basic`); + * `"body"` rejects any + * request that carries HTTP Basic (forces `client_secret_post`). Omitting it + * keeps the permissive default that accepts either. */ + readonly requireClientAuthMethod?: "basic" | "body"; } export interface OAuthTestServerShape { @@ -93,6 +100,11 @@ export interface OAuthTestServerShape { readonly requests: Effect.Effect; readonly clearRequests: Effect.Effect; readonly issuedAccessTokens: Effect.Effect; + /** Ordered log of which client-auth transport (`"basic"` | `"body"`) each + * `/token` request used, by arrival order; reset by `clearRequests`. Use it + * to assert that `exchangeClientCredentials` / `refreshAccessToken` actually + * sent credentials via HTTP Basic rather than in the form body. */ + readonly tokenRequestAuthMethods: Effect.Effect; readonly acceptsAccessToken: (token: string) => Effect.Effect; readonly acceptsAuthorizationHeader: ( authorization: string | null | undefined, @@ -168,6 +180,21 @@ const parseJsonObject = (body: string): Readonly> | null const arrayOfStrings = (value: unknown): readonly string[] => Array.isArray(value) ? value.filter((item): item is string => typeof item === "string") : []; +// RFC 6749 §2.3.1: the client id and secret in an HTTP Basic header are each +// `application/x-www-form-urlencoded` BEFORE base64, so a spec-compliant server +// must form-decode them after splitting. oauth4webapi's ClientSecretBasic +// percent-encodes (e.g. `test-client` -> `test%2Dclient`); without decoding, +// the id would never match (the body/`client_secret_post` path gets this for +// free via URLSearchParams). Falls back to the raw slice on malformed input. +const formDecode = (value: string): string => { + // oxlint-disable-next-line executor/no-try-catch-or-throw -- boundary: decodeURIComponent throws on malformed percent-escapes; fall back to raw + try { + return decodeURIComponent(value.replace(/\+/g, " ")); + } catch { + return value; + } +}; + const decodeBasicAuthorization = ( value: string | undefined, ): { readonly username: string; readonly password: string } | null => { @@ -178,8 +205,8 @@ const decodeBasicAuthorization = ( const separator = decoded.indexOf(":"); if (separator < 0) return null; return { - username: decoded.slice(0, separator), - password: decoded.slice(separator + 1), + username: formDecode(decoded.slice(0, separator)), + password: formDecode(decoded.slice(separator + 1)), }; }; @@ -422,6 +449,8 @@ export const serveOAuthTestServer = ( Effect.gen(function* () { const requests = yield* Ref.make([]); const issuedAccessTokens = yield* Ref.make>(new Set()); + const tokenRequestAuthMethods = yield* Ref.make([]); + const requireClientAuthMethod = options.requireClientAuthMethod; const users = { [options.defaultUsername ?? "alice"]: options.defaultPassword ?? "password", ...(options.users ?? {}), @@ -621,6 +650,25 @@ export const serveOAuthTestServer = ( if (requestUrl.pathname === "/token" && request.method === "POST") { const params = new URLSearchParams(body); const basic = decodeBasicAuthorization(headers.authorization); + // Record which client-auth transport this /token call used, then (when + // configured) enforce a required one. Recording happens for ALL grant + // types so tests can assert the transport on any flow. + const usedAuthMethod: "basic" | "body" = basic ? "basic" : "body"; + yield* Ref.update(tokenRequestAuthMethods, (all) => [...all, usedAuthMethod]); + if (requireClientAuthMethod === "basic" && !basic) { + return oauthError( + 401, + "invalid_client", + "This authorization server requires HTTP Basic client authentication", + ); + } + if (requireClientAuthMethod === "body" && basic) { + return oauthError( + 401, + "invalid_client", + "This authorization server requires client_secret_post (body) client authentication", + ); + } const clientId = basic?.username ?? params.get("client_id"); const clientSecret = basic?.password ?? params.get("client_secret"); const client = clientId ? clients.get(clientId) : undefined; @@ -771,8 +819,11 @@ export const serveOAuthTestServer = ( tokenEndpoint: `${issuerUrl}/token`, }), requests: Ref.get(requests), - clearRequests: Ref.set(requests, []), + clearRequests: Effect.all([Ref.set(requests, []), Ref.set(tokenRequestAuthMethods, [])]).pipe( + Effect.asVoid, + ), issuedAccessTokens: accessTokenSet.pipe(Effect.map((tokens) => [...tokens])), + tokenRequestAuthMethods: Ref.get(tokenRequestAuthMethods), acceptsAccessToken: (token) => accessTokenSet.pipe(Effect.map((tokens) => tokens.has(token))), acceptsAuthorizationHeader: (authorization) => { const token = authorization?.replace(/^Bearer\s+/i, ""); diff --git a/packages/react/src/api/atoms.tsx b/packages/react/src/api/atoms.tsx index 0f8ff57f9..a4009afb4 100644 --- a/packages/react/src/api/atoms.tsx +++ b/packages/react/src/api/atoms.tsx @@ -502,6 +502,7 @@ export const createOAuthClientOptimistic = oauthClientsOptimisticAtom.pipe( readonly clientId: string; readonly resource?: string | null; readonly originIntegration?: IntegrationSlug | null; + readonly tokenEndpointAuthMethod?: "body" | "basic"; }; }, ) => @@ -514,6 +515,9 @@ export const createOAuthClientOptimistic = oauthClientsOptimisticAtom.pipe( tokenUrl: arg.payload.tokenUrl, resource: arg.payload.resource ?? null, clientId: arg.payload.clientId, + ...(arg.payload.tokenEndpointAuthMethod === "basic" + ? { tokenEndpointAuthMethod: "basic" as const } + : {}), // Mirror the server's stamp so the just-registered app matches its // integration in the picker immediately (before the refetch lands). origin: { kind: "manual", integration: arg.payload.originIntegration ?? null }, diff --git a/packages/react/src/components/add-account-modal.tsx b/packages/react/src/components/add-account-modal.tsx index 7259b199c..34f0c45ea 100644 --- a/packages/react/src/components/add-account-modal.tsx +++ b/packages/react/src/components/add-account-modal.tsx @@ -2188,6 +2188,9 @@ function AddAccountModalView(props: AddAccountModalProps) { tokenUrl: editingClient.tokenUrl, resource: editingClient.resource ?? null, grant: editingClient.grant, + // Carry the stored auth method so editing a Basic client and + // saving does not silently rewrite the row to body. + tokenEndpointAuthMethod: editingClient.tokenEndpointAuthMethod, clientId: editingClient.clientId, }} onCreated={() => setEditingClient(null)} diff --git a/packages/react/src/components/oauth-client-form.tsx b/packages/react/src/components/oauth-client-form.tsx index 9d73c881d..9f1d7eb50 100644 --- a/packages/react/src/components/oauth-client-form.tsx +++ b/packages/react/src/components/oauth-client-form.tsx @@ -4,6 +4,7 @@ import * as Exit from "effect/Exit"; import { ExternalLink } from "lucide-react"; import { OAuthClientSlug, + type ClientAuthMethod, type IntegrationSlug, type OAuthGrant, type Owner, @@ -56,6 +57,9 @@ export interface OAuthClientFormPrefill { * and they are only sent when no declared scopes exist. */ readonly discoveredScopes?: readonly string[]; readonly grant?: OAuthGrant; + /** Token-endpoint client-auth method to preselect ("body" | "basic"). + * Defaults to "body" (client_secret_post). */ + readonly tokenEndpointAuthMethod?: ClientAuthMethod; /** Client id to seed (e.g. when editing an existing app). NOT a secret — the * secret is never returned, so it is always re-entered. */ readonly clientId?: string; @@ -180,6 +184,9 @@ export function OAuthClientForm(props: { ); const [name, setName] = useState(integrationName); const [grant, setGrant] = useState(prefill?.grant ?? "authorization_code"); + const [tokenEndpointAuthMethod, setTokenEndpointAuthMethod] = useState( + prefill?.tokenEndpointAuthMethod ?? "body", + ); const [clientId, setClientId] = useState(prefill?.clientId ?? ""); const [clientSecret, setClientSecret] = useState(""); const [issuerUrl, setIssuerUrl] = useState(""); @@ -338,6 +345,9 @@ export function OAuthClientForm(props: { clientId: clientId.trim(), clientSecret: clientSecret.trim(), resource, + ...(tokenEndpointAuthMethod === "basic" + ? { tokenEndpointAuthMethod: "basic" as const } + : {}), // Editing preserves the app's already-recorded origin (via // `intentIntegration`, passed verbatim by the caller); a fresh // registration from an integration's dialog stamps recorded intent. @@ -545,6 +555,53 @@ export function OAuthClientForm(props: { + {/* token-endpoint client auth, only relevant when a secret is sent. + Most providers accept client_secret_post; some require + client_secret_basic. Hidden for public clients with no secret. */} + {clientSecret.trim().length > 0 || grant === "client_credentials" ? ( +
+ + setTokenEndpointAuthMethod(next as ClientAuthMethod)} + className="gap-2" + > + {( + [ + { + value: "body", + label: "Request body", + hint: "client_secret_post (default)", + }, + { + value: "basic", + label: "HTTP Basic", + hint: "client_secret_basic", + }, + ] as const + ).map((option) => ( + + ))} + +
+ ) : null} + {/* endpoints */} {endpointsKnown && !showEndpoints ? (