@zackbart/connecta 0.28.1 → 0.30.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +855 -5
- package/README.md +125 -116
- package/bin/connecta.mjs +121 -139
- package/bin/meta-tool-names.d.mts +2 -0
- package/bin/meta-tool-names.mjs +9 -0
- package/bin/version.mjs +3 -0
- package/dist/access-tokens.js +53 -63
- package/dist/activity-request.js +8 -0
- package/dist/activity.d.ts +76 -42
- package/dist/activity.js +110 -42
- package/dist/argument-redaction.js +230 -0
- package/dist/auth/clerk-sdk/client.d.ts +65 -0
- package/dist/auth/clerk-sdk/client.js +5 -0
- package/dist/auth/clerk-sdk/generated.js +1845 -0
- package/dist/auth/clerk-transport.js +19 -0
- package/dist/auth/clerk.d.ts +17 -7
- package/dist/auth/clerk.js +189 -107
- package/dist/auth/cloudflare-access.js +25 -26
- package/dist/auth/downstream-client-metadata.d.ts +29 -0
- package/dist/auth/downstream-client-metadata.js +133 -0
- package/dist/auth/downstream-oauth.js +1584 -1580
- package/dist/auth/downstream-revocation.js +106 -0
- package/dist/auth/oauth-token-response.js +231 -0
- package/dist/auth/oauth-v2-migration.js +116 -0
- package/dist/auth/static-oauth.js +93 -71
- package/dist/auth-elicitation.js +288 -0
- package/dist/authorization-handoff.js +111 -0
- package/dist/branding.d.ts +8 -16
- package/dist/branding.js +26 -59
- package/dist/byte-read-response.js +39 -0
- package/dist/call-admission.js +12 -16
- package/dist/catalog-cache.js +739 -0
- package/dist/catalog-drift.d.ts +50 -15
- package/dist/catalog-drift.js +365 -128
- package/dist/catalog-fingerprint.js +2 -4
- package/dist/catalog-freshness.js +13 -0
- package/dist/catalog-limits.js +2 -2
- package/dist/catalog-service.js +409 -225
- package/dist/catalog.js +43 -84
- package/dist/concurrency.js +3 -4
- package/dist/config-defaults.d.ts +39 -0
- package/dist/config-defaults.js +46 -0
- package/dist/config-schema.d.ts +181 -0
- package/dist/config-schema.js +435 -0
- package/dist/config-value-sources.js +102 -0
- package/dist/config.d.ts +633 -0
- package/dist/config.js +508 -0
- package/dist/connector-access.d.ts +1 -0
- package/dist/connector-access.js +13 -15
- package/dist/connector-caller.d.ts +8 -0
- package/dist/connector-caller.js +15 -6
- package/dist/connector-scope.js +1 -1
- package/dist/connectors/api-connector.d.ts +59 -7
- package/dist/connectors/api-connector.js +106 -19
- package/dist/connectors/api.js +4 -1
- package/dist/connectors/guarded-fetch.d.ts +144 -0
- package/dist/connectors/guarded-fetch.js +87 -38
- package/dist/connectors/negotiation-cache.js +46 -0
- package/dist/connectors/option-shapes.js +27 -0
- package/dist/connectors/remote-mcp.d.ts +30 -4
- package/dist/connectors/remote-mcp.js +1774 -320
- package/dist/connectors/resource-uri.js +335 -0
- package/dist/credential-contract.d.ts +10 -0
- package/dist/credential-rules.js +1 -3
- package/dist/credential-url.js +108 -0
- package/dist/credentials.d.ts +6 -1
- package/dist/credentials.js +27 -18
- package/dist/d1.d.ts +130 -0
- package/dist/d1.js +290 -0
- package/dist/describe-config.d.ts +165 -0
- package/dist/describe-config.js +315 -0
- package/dist/described.js +106 -0
- package/dist/downstream-input-context.js +128 -0
- package/dist/downstream-input.js +325 -0
- package/dist/errors.d.ts +132 -31
- package/dist/errors.js +175 -87
- package/dist/execute.js +798 -318
- package/dist/executor-admission.d.ts +1 -0
- package/dist/executor-admission.js +11 -15
- package/dist/executor-contract.d.ts +20 -0
- package/dist/executor-contract.js +48 -0
- package/dist/executor-result.js +30 -13
- package/dist/executors/quickjs-child.js +15 -9
- package/dist/executors/quickjs-protocol.js +5 -6
- package/dist/executors/quickjs-runtime.js +152 -59
- package/dist/executors/quickjs.js +49 -39
- package/dist/favicon.js +1 -2
- package/dist/fix-prompt.js +1 -3
- package/dist/guest-runtime.js +238 -0
- package/dist/guest-types.d.ts +218 -0
- package/dist/guest-types.js +1 -0
- package/dist/html-security.js +24 -0
- package/dist/identity.js +1 -3
- package/dist/inbound-credential.js +52 -0
- package/dist/index.d.ts +24 -256
- package/dist/index.js +65 -478
- package/dist/invocation-auth.js +69 -0
- package/dist/invocation.js +262 -79
- package/dist/json-schema.d.ts +1 -1
- package/dist/mcp-client-context.js +35 -0
- package/dist/mcp-result.js +11 -7
- package/dist/meta-output.js +61 -0
- package/dist/meta-tool-names.d.ts +2 -0
- package/dist/meta-tool-names.js +3 -0
- package/dist/meta-tools.js +419 -272
- package/dist/module-contracts.d.ts +8 -23
- package/dist/node.d.ts +0 -2
- package/dist/node.js +2 -2
- package/dist/oauth-callback-outcome.js +1 -3
- package/dist/oauth-handoff.js +194 -0
- package/dist/oauth-sealing.js +20 -0
- package/dist/operator-record.d.ts +137 -0
- package/dist/operator-record.js +420 -0
- package/dist/operator-ui/assets.js +31 -0
- package/dist/operator-ui/contract.d.ts +72 -0
- package/dist/operator-ui/contract.js +1 -0
- package/dist/operator-ui/fix-prompts.d.ts +18 -0
- package/dist/operator-ui/generated.js +5 -4
- package/dist/operator-ui/model.d.ts +12 -87
- package/dist/operator-ui/model.js +2 -9
- package/dist/operator-ui/setup-commands.js +1 -3
- package/dist/operator-ui/view.d.ts +323 -0
- package/dist/operator-ui/view.js +62 -189
- package/dist/page-styles.js +2 -3
- package/dist/program-result.js +5 -0
- package/dist/program-source.js +14 -2
- package/dist/{exempt-writes.js → program-writes.js} +60 -19
- package/dist/provider.d.ts +142 -0
- package/dist/provider.js +187 -0
- package/dist/providers/_shared/google/result-size.js +57 -0
- package/dist/providers/_shared/google/service-account.d.ts +58 -0
- package/dist/providers/_shared/google/service-account.js +500 -0
- package/dist/providers/_shared/google/workspace.d.ts +276 -0
- package/dist/providers/_shared/google/workspace.js +518 -0
- package/dist/providers/_shared/request-token.js +83 -0
- package/dist/providers/{basecamp.d.ts → basecamp/index.d.ts} +6 -28
- package/dist/providers/basecamp/index.js +997 -0
- package/dist/providers/basecamp/skill.generated.js +9 -0
- package/dist/providers/{breeze.d.ts → breeze/index.d.ts} +2 -2
- package/dist/providers/{breeze.js → breeze/index.js} +184 -140
- package/dist/providers/breeze/skill.generated.js +9 -0
- package/dist/providers/{ccb.d.ts → ccb/index.d.ts} +2 -2
- package/dist/providers/{ccb.js → ccb/index.js} +271 -116
- package/dist/providers/ccb/skill.generated.js +9 -0
- package/dist/providers/{cloudflare.d.ts → cloudflare/index.d.ts} +8 -7
- package/dist/providers/{cloudflare.js → cloudflare/index.js} +171 -738
- package/dist/providers/cloudflare/skill.generated.js +10 -0
- package/dist/providers/docs/index.d.ts +54 -0
- package/dist/providers/docs/index.js +1237 -0
- package/dist/providers/docs/skill.generated.js +9 -0
- package/dist/providers/drive/index.d.ts +53 -0
- package/dist/providers/drive/index.js +1523 -0
- package/dist/providers/drive/skill.generated.js +9 -0
- package/dist/providers/forms/index.d.ts +54 -0
- package/dist/providers/forms/index.js +1217 -0
- package/dist/providers/forms/skill.generated.js +11 -0
- package/dist/providers/github/auth.d.ts +32 -0
- package/dist/providers/github/auth.js +361 -0
- package/dist/providers/github/hosted.js +218 -0
- package/dist/providers/github/index.d.ts +13 -0
- package/dist/providers/github/index.js +140 -0
- package/dist/providers/github/key.js +100 -0
- package/dist/providers/github/rest.js +321 -0
- package/dist/providers/github/scope.d.ts +29 -0
- package/dist/providers/github/scope.js +124 -0
- package/dist/providers/github/skill.generated.js +8 -0
- package/dist/providers/gmail/index.d.ts +51 -0
- package/dist/providers/gmail/index.js +1436 -0
- package/dist/providers/gmail/skill.generated.js +9 -0
- package/dist/providers/infisical/index.d.ts +7 -0
- package/dist/providers/infisical/index.js +713 -0
- package/dist/providers/infisical/skill.generated.js +8 -0
- package/dist/providers/{linear.d.ts → linear/index.d.ts} +11 -14
- package/dist/providers/linear/index.js +352 -0
- package/dist/providers/linear/skill.generated.js +8 -0
- package/dist/providers/{mixpanel.d.ts → mixpanel/index.d.ts} +5 -10
- package/dist/providers/mixpanel/index.js +416 -0
- package/dist/providers/mixpanel/skill.generated.js +9 -0
- package/dist/providers/{notion.d.ts → notion/index.d.ts} +8 -8
- package/dist/providers/{notion.js → notion/index.js} +226 -239
- package/dist/providers/notion/skill.generated.js +10 -0
- package/dist/providers/{overflow.d.ts → overflow/index.d.ts} +2 -2
- package/dist/providers/{overflow.js → overflow/index.js} +74 -111
- package/dist/providers/overflow/skill.generated.js +8 -0
- package/dist/providers/{planning-center.d.ts → planning-center/index.d.ts} +2 -2
- package/dist/providers/{planning-center.js → planning-center/index.js} +264 -104
- package/dist/providers/planning-center/skill.generated.js +10 -0
- package/dist/providers/{revenuecat.d.ts → revenuecat/index.d.ts} +5 -10
- package/dist/providers/revenuecat/index.js +557 -0
- package/dist/providers/revenuecat/skill.generated.js +11 -0
- package/dist/providers/sheets/index.d.ts +54 -0
- package/dist/providers/sheets/index.js +1237 -0
- package/dist/providers/sheets/skill.generated.js +10 -0
- package/dist/providers/slides/index.d.ts +53 -0
- package/dist/providers/slides/index.js +2645 -0
- package/dist/providers/slides/skill.generated.js +9 -0
- package/dist/providers/{stripe.d.ts → stripe/index.d.ts} +5 -12
- package/dist/providers/stripe/index.js +266 -0
- package/dist/providers/stripe/skill.generated.js +11 -0
- package/dist/providers/{tithely.d.ts → tithely/index.d.ts} +2 -2
- package/dist/providers/{tithely.js → tithely/index.js} +60 -91
- package/dist/providers/tithely/skill.generated.js +8 -0
- package/dist/providers/{vercel.d.ts → vercel/index.d.ts} +8 -8
- package/dist/providers/vercel/index.js +1038 -0
- package/dist/providers/vercel/mcp-ownership.js +212 -0
- package/dist/providers/vercel/skill.generated.js +11 -0
- package/dist/registry.d.ts +110 -171
- package/dist/registry.js +588 -927
- package/dist/request-state.js +21 -0
- package/dist/resource-read.js +103 -0
- package/dist/result-shapes.js +17 -49
- package/dist/retired-toolkits.js +1 -1
- package/dist/routes/access-tokens.js +3 -6
- package/dist/routes/activity-context.js +21 -0
- package/dist/routes/activity-disclosure.js +40 -0
- package/dist/routes/activity.js +59 -21
- package/dist/routes/actor-labels.js +3 -50
- package/dist/routes/connect.js +221 -0
- package/dist/routes/credentials.js +15 -27
- package/dist/routes/mcp.js +365 -130
- package/dist/routes/oauth-client-metadata.js +15 -0
- package/dist/routes/oauth-management.js +45 -107
- package/dist/routes/oauth.js +110 -57
- package/dist/routes/operator.js +3 -3
- package/dist/routes/shared.d.ts +25 -64
- package/dist/routes/shared.js +160 -80
- package/dist/routes/skills.js +30 -0
- package/dist/routes/ui-config.js +225 -0
- package/dist/routes/ui-data.js +40 -35
- package/dist/routes/ui.js +66 -56
- package/dist/runtime/admission.js +4 -2
- package/dist/runtime/call-admission.js +3 -1
- package/dist/runtime/connector-scope.js +1 -4
- package/dist/runtime/run.js +10 -39
- package/dist/runtime/services.js +12 -117
- package/dist/runtime/shared-read.js +1 -3
- package/dist/runtime/storage.js +8 -1
- package/dist/sent-secrets.d.ts +93 -0
- package/dist/sent-secrets.js +1013 -0
- package/dist/server.js +28 -71
- package/dist/skills.js +416 -174
- package/dist/sqlite.d.ts +48 -0
- package/dist/sqlite.js +181 -0
- package/dist/storage/keys.js +443 -0
- package/dist/storage/memory.js +16 -4
- package/dist/storage/sql.d.ts +66 -0
- package/dist/storage/sql.js +543 -0
- package/dist/timeout.js +4 -2
- package/dist/tool-name.js +17 -0
- package/dist/tool-safety.d.ts +9 -35
- package/dist/tool-safety.js +20 -40
- package/dist/types.d.ts +336 -56
- package/dist/types.js +0 -1
- package/dist/ui.d.ts +7 -19
- package/dist/ui.js +76 -90
- package/dist/url-safety.js +3 -6
- package/dist/usage-guide.js +146 -0
- package/dist/validate.d.ts +7 -0
- package/dist/validate.js +309 -47
- package/dist/version.d.ts +1 -7
- package/dist/version.js +3 -7
- package/dist/worker.d.ts +11 -0
- package/dist/worker.js +435 -0
- package/documentation/README.md +63 -0
- package/documentation/architecture.md +696 -231
- package/documentation/auth.md +1069 -546
- package/documentation/code-mode.md +662 -356
- package/documentation/deploying.md +146 -0
- package/documentation/integrating.md +159 -0
- package/documentation/meta-tools.md +570 -216
- package/documentation/operating.md +135 -0
- package/documentation/operator-tests.md +97 -0
- package/documentation/operator-ui.md +132 -0
- package/documentation/provider-migration-0.29.md +65 -0
- package/examples/worker/AGENTS.md +34 -12
- package/examples/worker/README.md +293 -255
- package/examples/worker/kv-to-d1.wrangler.jsonc +26 -0
- package/examples/worker/scripts/copy-kv-to-d1.mjs +162 -0
- package/examples/worker/src/connecta.config.ts +151 -0
- package/examples/worker/src/index.ts +14 -197
- package/examples/worker/wrangler.jsonc +15 -35
- package/package.json +126 -54
- package/templates/node/.dockerignore +1 -2
- package/templates/node/.env.example +20 -21
- package/templates/node/AGENTS.md +21 -17
- package/templates/node/Dockerfile +3 -5
- package/templates/node/README.md +140 -58
- package/templates/node/docker-compose.yml +11 -12
- package/templates/node/package.json +4 -2
- package/templates/node/src/connecta.config.ts +119 -0
- package/templates/node/src/index.ts +9 -154
- package/templates/node/src/provision-token.ts +17 -0
- package/AGENTS.md +0 -170
- package/dist/artifacts/connector.d.ts +0 -58
- package/dist/artifacts/connector.js +0 -730
- package/dist/artifacts/document.js +0 -86
- package/dist/artifacts/guide.js +0 -140
- package/dist/artifacts/html-scan.js +0 -329
- package/dist/artifacts/json.js +0 -78
- package/dist/artifacts/kv-store.d.ts +0 -20
- package/dist/artifacts/kv-store.js +0 -140
- package/dist/artifacts/markdown.js +0 -478
- package/dist/artifacts/module.d.ts +0 -37
- package/dist/artifacts/module.js +0 -66
- package/dist/artifacts/operations.d.ts +0 -197
- package/dist/artifacts/operations.js +0 -1002
- package/dist/artifacts/refresh.d.ts +0 -55
- package/dist/artifacts/refresh.js +0 -236
- package/dist/artifacts/routes.js +0 -206
- package/dist/artifacts/types.d.ts +0 -214
- package/dist/artifacts/types.js +0 -22
- package/dist/artifacts/validate.d.ts +0 -62
- package/dist/artifacts/validate.js +0 -651
- package/dist/artifacts.d.ts +0 -6
- package/dist/artifacts.js +0 -9
- package/dist/auth/bearer.d.ts +0 -11
- package/dist/auth/bearer.js +0 -50
- package/dist/providers/basecamp.js +0 -445
- package/dist/providers/linear.js +0 -217
- package/dist/providers/mixpanel.js +0 -247
- package/dist/providers/revenuecat.js +0 -317
- package/dist/providers/stripe.js +0 -253
- package/dist/providers/vercel.js +0 -1253
- package/dist/storage/file.d.ts +0 -15
- package/dist/storage/file.js +0 -455
- package/ethos.md +0 -93
- package/examples/worker/src/cloudflare-kv.ts +0 -43
- package/examples/worker/src/d1-activity-row.ts +0 -155
- package/examples/worker/src/d1-activity.ts +0 -147
- package/examples/worker/src/d1-storage.ts +0 -138
- package/examples/worker/src/r2-artifact-blobs.ts +0 -29
- package/templates/node/src/file-activity.ts +0 -182
package/documentation/auth.md
CHANGED
|
@@ -1,31 +1,83 @@
|
|
|
1
1
|
# Inbound auth
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
3
|
+
## Task routes
|
|
4
|
+
|
|
5
|
+
[Agent index](./README.md) distinguishes endpoint operation from deployment
|
|
6
|
+
and package maintenance. This guide owns identity, grants, and downstream
|
|
7
|
+
credential contracts; use [deployment setup](./deploying.md) for runnable steps.
|
|
8
|
+
|
|
9
|
+
| Task | Sections |
|
|
10
|
+
| ----------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
11
|
+
| Connect a machine or migrate an old client | [Managed tokens](#managed-client-tokens-and-upgrading-from-v023), [static bearer migration](#migrating-static-bearer-clients) |
|
|
12
|
+
| Configure caller visibility, trust, or auth ownership | [Principals and operators](#principals-visibility-and-operators), [pools](#pools), [shared and personal auth](#shared-and-personal-auth) |
|
|
13
|
+
| Set up human sign-in | [Access on Workers](#cloudflare-access-on-workers), [Clerk tokens and sessions](#clerk-oauth-tokens-and-operator-sessions), [management](#human-authentication-management) |
|
|
14
|
+
| Enable credential or connection controls | [Management permissions](#management-permissions), [start/restart/disconnect](#starting-restarting-and-disconnecting) |
|
|
15
|
+
| Diagnose consent or token refresh | [State at rest](#downstream-oauth-state-at-rest), [callbacks](#consents-and-callbacks), [refresh failures](#refresh-failures), [safe errors](#what-a-servers-errors-may-say) |
|
|
16
|
+
| Configure a downstream OAuth client or HTTP handler | [Client registration](#downstream-oauth-client-registration), [api() OAuth](#downstream-oauth-on-api), [request-local bearer tokens](#request-local-downstream-bearer-tokens) |
|
|
17
|
+
| Review browser or downstream URL boundaries | [Origins](#origins), [advertised URLs](#urls-a-downstream-advertises), [one server per grant](#one-server-per-grant) |
|
|
18
|
+
|
|
19
|
+
[Credential recognition and admission](#credential-recognition-and-admission)
|
|
20
|
+
defines refusal precedence.
|
|
21
|
+
[Source and tests](#source-and-tests) points to the implementation. Migration
|
|
22
|
+
sections describe upgrade procedures, not additional auth modes to enable.
|
|
23
|
+
|
|
24
|
+
## Credential recognition and admission
|
|
25
|
+
|
|
26
|
+
Inbound auth decides who may reach the MCP endpoint. People use `clerkAuth`
|
|
27
|
+
from `@zackbart/connecta/auth/clerk` or `cloudflareAccessAuth` from
|
|
28
|
+
`/auth/cloudflare-access`. Machines use `accessTokens(storage)` from
|
|
29
|
+
`/auth/access-tokens`, which installs the `cta_` adapter before human providers.
|
|
30
|
+
Access is Workers-only; Node uses Clerk.
|
|
31
|
+
|
|
32
|
+
An `InboundAuth` provider recognizes credential syntax or trusted runtime
|
|
33
|
+
context with synchronous `recognizesCredential(request, runtimeContext)`.
|
|
34
|
+
Recognition does no verification or I/O. The first recognizing provider owns
|
|
35
|
+
the request, including refusals, so an invalid machine credential cannot fall
|
|
36
|
+
back to an ambient browser identity. An explicit Authorization header is
|
|
37
|
+
decisive on every protected route. A shared parser normalizes the
|
|
38
|
+
case-insensitive Bearer scheme; unsupported schemes, malformed spacing, empty
|
|
39
|
+
values, combined duplicate headers, and verification failures return 401.
|
|
40
|
+
Cookies, Clerk browser handshake credentials, and trusted Access context are
|
|
41
|
+
excluded from that request. Access cannot verify caller headers and refuses
|
|
42
|
+
them. If no provider recognizes a valid header, the first eligible provider
|
|
43
|
+
owns its verdict. Without an Authorization header, providers are tried in
|
|
44
|
+
configuration order until admission or a response other than 401.
|
|
45
|
+
Browser OAuth starts and callbacks retain the 401 challenge for an explicit
|
|
46
|
+
header refusal.
|
|
47
|
+
Custom providers can omit recognition when they supply no distinct credential
|
|
48
|
+
syntax; providers with credentials should implement it. A recognition throw
|
|
49
|
+
refuses the request without logging the thrown text.
|
|
50
|
+
|
|
51
|
+
The `cta_` syntax is reserved even without the optional token verifier: it
|
|
52
|
+
refuses rather than becoming an ambient human or an open anonymous request.
|
|
53
|
+
Async recognition hooks fail at construction; unexpected rejected promises
|
|
54
|
+
are consumed before refusal so their text cannot reach runtime output.
|
|
55
|
+
Human routes reject recognized machine credentials without consulting token
|
|
56
|
+
storage, then ask only interactive providers. Replace custom `final` and
|
|
57
|
+
`finalRefusals` handling with recognition; handshake redirects and admission
|
|
58
|
+
policy refusals are provider verdicts too. Recognition grants no identity.
|
|
59
|
+
|
|
60
|
+
`authorize` returns `{ ok: true, userId?, subjectId?, principal? }` or a refusal
|
|
61
|
+
carrying a `Response`. On 401, routing and challenge selection use the first
|
|
62
|
+
provider that actually answers the endpoint's protected-resource metadata
|
|
63
|
+
request. Its `challenge(request, baseUrl)` supplies `WWW-Authenticate`, including
|
|
64
|
+
`scope`; an unrelated metadata hook does not own resource discovery. Machine-only
|
|
65
|
+
endpoints challenge with `Bearer` and advertise no authorization server. With
|
|
66
|
+
Clerk, even a refused `cta_` token receives Clerk's resource metadata challenge.
|
|
67
|
+
An open deployment with connectors warns at construction.
|
|
68
|
+
|
|
69
|
+
Clerk denial logs carry checked fixed reason codes. Provider user IDs, email
|
|
70
|
+
addresses, and email domains stay out of denial logs, activity, and status,
|
|
71
|
+
including email lookup failures and admission policy refusals.
|
|
18
72
|
|
|
19
73
|
## Managed client tokens and upgrading from v0.23
|
|
20
74
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
module loads none of its implementation and serves none of those routes.
|
|
75
|
+
`accessTokens` from `@zackbart/connecta/auth/access-tokens` installs its inbound
|
|
76
|
+
adapter and, beside `ui`, the Access tokens page and `/ui/access-tokens`
|
|
77
|
+
routes. Omitted, none of it loads or is served.
|
|
25
78
|
|
|
26
79
|
```ts
|
|
27
80
|
import { accessTokens } from "@zackbart/connecta/auth/access-tokens";
|
|
28
|
-
|
|
29
81
|
createConnecta({
|
|
30
82
|
storage,
|
|
31
83
|
accessTokens: accessTokens(storage),
|
|
@@ -37,46 +89,41 @@ createConnecta({
|
|
|
37
89
|
principal?.namespace === "clerk:your-existing-namespace" &&
|
|
38
90
|
principal.id === "your-operator-id",
|
|
39
91
|
},
|
|
40
|
-
connectors,
|
|
41
|
-
executor,
|
|
92
|
+
connectors, executor,
|
|
42
93
|
});
|
|
43
94
|
```
|
|
44
95
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
`
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
consistent store makes revocation effective on the next authorization; an
|
|
77
|
-
eventually consistent backend retains its own propagation delay. Requests already
|
|
78
|
-
admitted are not recalled. Management writes require an exact same-origin Origin,
|
|
79
|
-
and responses are private and non-cacheable.
|
|
96
|
+
From v0.23, replace `accessTokens: true` or its options object with
|
|
97
|
+
`accessTokens(storage)` on **the same storage and key namespace**, keeping the
|
|
98
|
+
`access-token:v1:record:*` and `access-token:v1:lookup:*` records: unrevoked
|
|
99
|
+
`cta_…` secrets keep working with no recovery, rewrite, or client rotation. Keep
|
|
100
|
+
the identity namespaces and connector/pool grant callbacks too; storage
|
|
101
|
+
compatibility translates no configuration and invents no grants. `access_token`
|
|
102
|
+
actor ids, the `connecta:access-tokens:v1` activity namespace, friendly names,
|
|
103
|
+
and stored principals carry over; a token without a principal stays unbound.
|
|
104
|
+
Every request evaluates current identity, tool, and pool grants, and a token
|
|
105
|
+
never becomes an interactive operator. Names are labels, never permissions. A
|
|
106
|
+
UI-issued token belongs to the issuing human's principal, and any explicitly
|
|
107
|
+
permitted token manager can list, rename, or revoke deployment tokens. Machine tokens manage neither tokens nor connection credentials; operators
|
|
108
|
+
need an interactive provider.
|
|
109
|
+
|
|
110
|
+
Issuance claims capacity with `compareAndSet`, because counting records before
|
|
111
|
+
writing admits too many concurrent creates; every supported store provides
|
|
112
|
+
it. Active
|
|
113
|
+
capacity defaults to 100, configurable with
|
|
114
|
+
`accessTokens(storage, { maxActive: 200 })` up to 1,000, counted by a durable
|
|
115
|
+
reservation before a secret is written. A failure before lookup publication
|
|
116
|
+
releases it; an uncertain lookup write keeps its metadata and capacity so an
|
|
117
|
+
operator can revoke it, even if creation returned no secret. Creation is never
|
|
118
|
+
retried automatically, and once new-version issuance starts, old-version
|
|
119
|
+
instances must create no tokens: they ignore reservations.
|
|
120
|
+
|
|
121
|
+
Secrets carry 256 random bits; only SHA-256 digests persist. Creation returns
|
|
122
|
+
the secret once, list and rename never. Revocation removes the lookup before
|
|
123
|
+
updating metadata and authorization caches no token, so revocation takes
|
|
124
|
+
effect on the next authorization (D1 and SQLite are strongly consistent);
|
|
125
|
+
admitted requests are not recalled. Management writes
|
|
126
|
+
require an exact same-origin Origin; responses are private and non-cacheable.
|
|
80
127
|
|
|
81
128
|
## Origins
|
|
82
129
|
|
|
@@ -92,60 +139,60 @@ silently never matching. A permitted origin still has to authenticate.
|
|
|
92
139
|
|
|
93
140
|
## Principals, visibility, and operators
|
|
94
141
|
|
|
95
|
-
One authorization yields three roles
|
|
96
|
-
activity, the **subject** owns transient results such as `
|
|
97
|
-
the **principal** is the human owner of personal connector auth. An
|
|
98
|
-
Clerk or Access user supplies all three;
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
142
|
+
One authorization yields three roles: the **actor** identifies the caller in
|
|
143
|
+
activity, the **subject** owns transient results such as `connecta.result` pages,
|
|
144
|
+
and the **principal** is the human owner of personal connector auth. An
|
|
145
|
+
interactive Clerk or Access user supplies all three; Access service identities are refused; machines present `cta_` tokens. The principal is an explicit
|
|
146
|
+
`principal: { namespace, id }` from `authorize`, accepted whenever it
|
|
147
|
+
validates, or else one derived from `userId` and the provider's
|
|
148
|
+
`activityActorNamespace`, so only the derived path needs a namespace.
|
|
149
|
+
|
|
150
|
+
A subject or user id always selects a result-stash partition; undeclared, the
|
|
151
|
+
namespace is `connecta:auth:<provider kind>`, where subject ids must be
|
|
152
|
+
distinct, and that fallback grants no personal-auth ownership and changes no
|
|
153
|
+
activity attribution. An explicit principal alone is the subject too. Open
|
|
154
|
+
deployments and providers returning no identity share one result partition.
|
|
155
|
+
|
|
156
|
+
A maintained provider that acts _as_ the caller downstream — `gmail()` through
|
|
157
|
+
Google Workspace domain-wide delegation — reads none of these roles on its own.
|
|
158
|
+
Its `subject` option, a deployment-config function from the admitted identity
|
|
159
|
+
to a downstream account (a Clerk principal id to a Workspace address, say) or
|
|
160
|
+
one fixed address, is applied once per call and only to a request an inbound
|
|
161
|
+
provider authenticated: any `ok`, a subject-less custom credential included, never an open
|
|
162
|
+
deployment's anonymous admission. Otherwise, or on `undefined`, the call fails
|
|
163
|
+
`auth_required` before any request leaves. Arguments, headers, and programs
|
|
164
|
+
never reach it; a deployment's own connectors still cannot read the caller.
|
|
165
|
+
|
|
166
|
+
`identity.connectorAccess` returns `"all"` (the default) or grants, for
|
|
167
|
+
discovery and use alike: a connector id opens every tool on it, a
|
|
168
|
+
`connector.tool` address that tool alone, and
|
|
169
|
+
`{ tool: "connector.tool", requireReadOnly: true }` that tool only while the
|
|
170
|
+
loaded catalog says it is explicitly read-only, never connector-wide. Grants
|
|
171
|
+
are additive, so a bare id or unrestricted address beside a guarded one wins.
|
|
121
172
|
|
|
122
173
|
Tool grants are enforced in the scoped registry view, below the catalog service,
|
|
123
|
-
so `search_tools`, both call tools, a program's
|
|
124
|
-
`connecta.describe`, and `connecta.call`, and the connection
|
|
125
|
-
filtered list. Connector-level discovery, guides, and
|
|
126
|
-
a connector when any
|
|
127
|
-
`docs`
|
|
128
|
-
returns the same "Unknown connector" refusal
|
|
129
|
-
ungranted tool fails exactly like one
|
|
130
|
-
with no hint that it exists. That is
|
|
131
|
-
one place on purpose.
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
bearer subject. What a request may never do is name its own scope.
|
|
174
|
+
so `search_tools`, `describe_tools`, both call tools, a program's
|
|
175
|
+
`connecta.search`, `connecta.describe`, and `connecta.call`, and the connection
|
|
176
|
+
UI read one filtered list. Connector-level discovery, guides, and
|
|
177
|
+
`authorize_connector` keep a connector when any of its tools is granted —
|
|
178
|
+
`docs.read` permits the `docs` handoff. Without one it is absent: discovery
|
|
179
|
+
omits it, and `authorize_connector` returns the same "Unknown connector" refusal
|
|
180
|
+
as for a connector that does not exist. An ungranted tool fails exactly like one
|
|
181
|
+
the connector never had: `unknown_tool`, with no hint that it exists. That is
|
|
182
|
+
the whole security claim, in one place on purpose. There is no _caller-selected_
|
|
183
|
+
tool set: a narrower slice is a branch in this resolver or a pool, and a bot
|
|
184
|
+
needing its own is its own stored token subject. A request may never name its own
|
|
185
|
+
scope.
|
|
136
186
|
|
|
137
187
|
### Team read sets
|
|
138
188
|
|
|
139
|
-
|
|
140
|
-
different views of one deployment. Keep membership in deployment code and
|
|
141
|
-
review the exact tool addresses you intend to grant:
|
|
189
|
+
Keep team membership in deployment code and review exact addresses:
|
|
142
190
|
|
|
143
191
|
```ts
|
|
144
192
|
const engineering = new Set(["engineer-id"]);
|
|
145
193
|
const marketing = new Set(["marketer-id"]);
|
|
146
194
|
const engineeringGrants = ["issues", { tool: "campaigns.list", requireReadOnly: true }] as const;
|
|
147
195
|
const marketingGrants = ["campaigns", { tool: "issues.search", requireReadOnly: true }] as const;
|
|
148
|
-
|
|
149
196
|
createConnecta({
|
|
150
197
|
auth: cloudflareAccessAuth(),
|
|
151
198
|
identity: {
|
|
@@ -158,73 +205,85 @@ createConnecta({
|
|
|
158
205
|
])];
|
|
159
206
|
},
|
|
160
207
|
},
|
|
161
|
-
connectors: [issues, campaigns, billing],
|
|
162
|
-
executor,
|
|
208
|
+
connectors: [issues, campaigns, billing], executor,
|
|
163
209
|
});
|
|
164
210
|
```
|
|
165
211
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
212
|
+
Engineering sees all `issues` tools and only `campaigns.list`; Marketing sees all
|
|
213
|
+
`campaigns` tools and only `issues.search`. Neither sees `billing`; both memberships
|
|
214
|
+
grant both connectors in full. Unknown or unprincipalled identities get `[]`.
|
|
215
|
+
Use a stable authenticated principal or subject, never request headers or tool
|
|
216
|
+
arguments. A token's stored principal is vouched for by inbound auth. Pools narrow their endpoint, not plain `/mcp`.
|
|
217
|
+
|
|
218
|
+
| Catalog change | Guarded exact grant | Unrestricted exact string grant |
|
|
219
|
+
| -------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------- |
|
|
220
|
+
| New name | Excluded until reviewed and added | Excluded until reviewed and added |
|
|
221
|
+
| Removed or renamed name | Unreachable; warned when the scoped view reads the catalog | Same |
|
|
222
|
+
| Stored write verdict after overrides and provider review | Removed from discovery and every invocation path, including `call_destructive_tool` and trusted programs | Still granted; `call_tool` refuses writes and write paths take over |
|
|
223
|
+
| Schema change alone | Does not revoke, unless it breaks a reviewed digest: then it is a write, as above | Same as above |
|
|
224
|
+
|
|
225
|
+
The example assumes both exact tools were reviewed as read-only; names imply no safety.
|
|
226
|
+
Review schemas, annotations, and downstream behavior
|
|
227
|
+
before changing lists or upgrading providers. Remote catalogs can drift without
|
|
228
|
+
upgrades; fresh cached facts are classified against the current review on every read; downstreams
|
|
229
|
+
can change behavior while keeping read-only annotations. For `api()`, the author
|
|
230
|
+
owns declarations and handlers. Connecta enforces loaded declarations, not
|
|
231
|
+
absence of side effects. Restricted downstream credentials do not replace grants.
|
|
232
|
+
|
|
233
|
+
`[]` hides all connectors from discovery and the connection UI. Address-only or
|
|
234
|
+
disqualified guarded grants can leave a connector visible with its tool unreachable.
|
|
235
|
+
Failed remote loads are errors, not empty catalogs. A transient first-page tool
|
|
236
|
+
listing failure may serve complete facts in the same auth partition for five
|
|
237
|
+
minutes after expiry. Failed pagination, permission and auth failures do not
|
|
238
|
+
use this fallback. The expired deadline cannot authorize auth recovery. Personal OAuth ownership and credential
|
|
239
|
+
administration are separate from visibility; program writes follow the endpoint's trust tier and host approval. `test/identity-scope.test.ts` exercises these boundaries.
|
|
240
|
+
|
|
241
|
+
## Migrating static bearer clients
|
|
242
|
+
|
|
243
|
+
The `/auth/bearer` export, `bearerToken`, and its `assertedPrincipal` header
|
|
244
|
+
mode are removed. Existing static secrets cannot be converted into `cta_`
|
|
245
|
+
secrets. Use a separate stored token for each machine or human owner:
|
|
246
|
+
|
|
247
|
+
1. Configure the existing D1 or SQLite store as `storage`, and install
|
|
248
|
+
`accessTokens(storage)`. Keep its existing keys and namespaces.
|
|
249
|
+
2. Sign in through Clerk or Worker Access. Grant the intended token manager
|
|
250
|
+
`identity.accessTokenManagement`, then create a token on Access tokens.
|
|
251
|
+
It is bound to that human's principal. For a machine without a human owner,
|
|
252
|
+
provision `new AccessTokenManager(storage).create("machine-name", "deployment-provisioning")` from
|
|
253
|
+
trusted deployment code. The Node template provides `npm run provision-token -- machine-name`.
|
|
254
|
+
3. Update `identity.connectorAccess` and pool grants to the returned token
|
|
255
|
+
metadata id with `actor.kind === "access_token"`. Names and `tokenPrefix`
|
|
256
|
+
are display labels. Keep existing human principal namespaces unchanged.
|
|
257
|
+
4. Save the once-returned secret in the client's secret store and send
|
|
258
|
+
`Authorization: Bearer cta_…`. Behind Worker Access, also satisfy the edge
|
|
259
|
+
with Access service credentials; an Access-only machine no longer admits.
|
|
260
|
+
5. Verify MCP initialization and the intended grants. Confirm the old static
|
|
261
|
+
secret refuses, then remove its environment secret and the old import.
|
|
262
|
+
Remove `X-Connecta-Principal`; a token's stored principal cannot vary by
|
|
263
|
+
request. Provision one human-bound token per represented user instead.
|
|
264
|
+
To retain an old asserted user's personal state, trusted provisioning must
|
|
265
|
+
call `manager.create(name, { namespace: existingNamespace, id: existingId })`
|
|
266
|
+
with the exact existing namespace and canonical id, after applying the old
|
|
267
|
+
admission policy. A Clerk UI token uses the Clerk principal instead and
|
|
268
|
+
cannot inherit a different asserted namespace's partitions.
|
|
269
|
+
|
|
270
|
+
Revocation acts on the next request. Rotate by creating a replacement,
|
|
271
|
+
updating the client, verifying it, and revoking the old token. Keep token text
|
|
272
|
+
out of logs and reports.
|
|
214
273
|
|
|
215
274
|
## Pools
|
|
216
275
|
|
|
217
276
|
A pool is a named slice of the deployment served at its own endpoint,
|
|
218
277
|
`/mcp/<pool>`, for when one identity needs different capability sets on different
|
|
219
278
|
clients: a support agent that sees three Notion tools and Linear, a calendar bot
|
|
220
|
-
that sees one tool, both over the same credentials
|
|
279
|
+
that sees one tool, both over the same credentials. Private remote catalog
|
|
280
|
+
cache entries remain partitioned by the admitted pool.
|
|
221
281
|
|
|
222
282
|
```ts
|
|
223
283
|
createConnecta({
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
],
|
|
284
|
+
storage,
|
|
285
|
+
accessTokens: accessTokens(storage),
|
|
286
|
+
auth: clerkAuth({ publishableKey, secretKey }),
|
|
228
287
|
pools: {
|
|
229
288
|
support: {
|
|
230
289
|
tools: ["linear", "notion.search_pages", "notion.fetch_page"],
|
|
@@ -232,50 +291,34 @@ createConnecta({
|
|
|
232
291
|
},
|
|
233
292
|
calendar_bot: {
|
|
234
293
|
tools: ["calendar.create_event"],
|
|
235
|
-
grant: ({ actor }) => actor.
|
|
294
|
+
grant: ({ actor }) => actor.kind === "access_token" && actor.id === calendarTokenId,
|
|
236
295
|
},
|
|
237
296
|
},
|
|
238
297
|
identity: { connectorAccess },
|
|
239
|
-
connectors,
|
|
240
|
-
executor,
|
|
298
|
+
connectors, executor,
|
|
241
299
|
});
|
|
242
300
|
```
|
|
243
301
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
succeeds — so pool names are not anonymously enumerable, and a valid credential
|
|
256
|
-
cannot tell the three cases apart by response content. Timing is not hidden: a
|
|
257
|
-
declared name awaits its grant while an undeclared name returns without that
|
|
258
|
-
lookup. We accept that oracle because names grant no access and a fixed delay
|
|
259
|
-
could not hide unbounded grant I/O anyway. Keep grants pure and fast; don't
|
|
260
|
-
treat pool names as secrets. The operator log carries the refusal reason.
|
|
261
|
-
- **Structural mistakes throw at construction.** A malformed name, an unknown
|
|
262
|
-
option, an unknown connector, an empty pool, and a `connector.tool` address an
|
|
263
|
-
`api()` connector's static catalog lacks all refuse to boot. Remote catalogs
|
|
264
|
-
load lazily, so their addresses are checked at load instead.
|
|
265
|
-
- **OAuth discovery follows the path.** On Clerk, the 401 challenge for
|
|
266
|
-
`/mcp/<pool>` names `/.well-known/oauth-protected-resource/mcp/<pool>`, whose
|
|
267
|
-
`resource` is the pool URL, so RFC 9728 clients see a match. Cloudflare
|
|
268
|
-
Managed OAuth is application-level and needs nothing.
|
|
269
|
-
|
|
270
|
-
An address the live catalog does not contain is unreachable and warned once
|
|
271
|
-
while it sits in a 1,024-entry FIFO, so caller-derived grant text cannot grow
|
|
272
|
-
retained warning state without bound. Later catalog drift can never widen a
|
|
273
|
-
grant: there is no wildcard, and every tool grant is an exact name.
|
|
302
|
+
Without configured `auth`, an open deployment has one anonymous, non-interactive
|
|
303
|
+
identity; grants like these return false and every pool path 404s.
|
|
304
|
+
|
|
305
|
+
| Rule | Contract |
|
|
306
|
+
| ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
307
|
+
| Scope | `/mcp/<pool>` intersects pool tools with the identity's `connectorAccess`, never widening it. Plain `/mcp` is unchanged; the resolver remains the security boundary. |
|
|
308
|
+
| Grant | Defaults to deny; only literal `true` admits. Other returns, throws, and undeclared names produce an identical 404 status, body, and headers, after auth. Names are not anonymously enumerable; valid credentials cannot distinguish these cases by content. |
|
|
309
|
+
| Timing | Declared names await grants, so timing is not hidden. Keep grants pure and fast and names unsecret; names grant no access. The operator log records the refusal reason. |
|
|
310
|
+
| Construction | Malformed names, unknown options or connectors, empty pools, and addresses absent from an `api()` static catalog throw. Remote catalogs are checked at lazy load. |
|
|
311
|
+
| Discovery | Clerk's pool 401 names `/.well-known/oauth-protected-resource/mcp/<pool>` with the pool URL as `resource`, matching RFC 9728. Cloudflare Managed OAuth is application-level and needs no pool setup. |
|
|
312
|
+
| Drift | No wildcards; tool grants are exact names. Missing live tools are unreachable and warned once while in a bounded 1,024-entry FIFO. |
|
|
274
313
|
|
|
275
314
|
## Shared and personal auth
|
|
276
315
|
|
|
277
316
|
Connector auth defaults to `authScope: "shared"` — its credential, OAuth state,
|
|
278
|
-
tokens
|
|
317
|
+
tokens and connector storage belong to the deployment. Private remote catalog
|
|
318
|
+
cache entries still include the admitted principal and pool; shared credentials
|
|
319
|
+
do not authorize cross-principal or cross-pool cache reuse. Public entries
|
|
320
|
+
require a downstream `cacheScope: "public"` declaration and contain only
|
|
321
|
+
intake-redacted successes, keyed by connector configuration. Set
|
|
279
322
|
`authScope: "personal"` when every human needs a separate downstream account:
|
|
280
323
|
|
|
281
324
|
```ts
|
|
@@ -284,157 +327,394 @@ remoteMcp("linear", { url: "https://mcp.linear.app/mcp", authScope: "personal",
|
|
|
284
327
|
```
|
|
285
328
|
|
|
286
329
|
A personal connector is absent — not refused — from any request without a
|
|
287
|
-
stable namespaced principal. For
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
and
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
construction.
|
|
330
|
+
stable namespaced principal. For one that can see it, connector storage, vault
|
|
331
|
+
records, OAuth generations, and observed result shapes are
|
|
332
|
+
partitioned under an opaque SHA-256 identity key. Remote catalog cache keys
|
|
333
|
+
include that owner and the admitted pool, plus the credential digest and OAuth
|
|
334
|
+
epoch; refreshes coalesce within
|
|
335
|
+
one owner, and another owner's epoch cannot retire them. Keep namespaces and
|
|
336
|
+
principal ids stable across upgrades, since changing either selects different
|
|
337
|
+
partitions. Literal `auth: { type: "headers" }` keeps its secret in deployment
|
|
338
|
+
code, so `remoteMcp()` refuses it as personal at construction.
|
|
296
339
|
|
|
297
340
|
## Downstream OAuth state at rest
|
|
298
341
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
keeps
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
342
|
+
Each owner of an OAuth connector (the deployment when shared, each principal
|
|
343
|
+
when personal) has one **grant record**, `oauth:grant` in the connector's
|
|
344
|
+
namespace: the live **epoch**, the latest consent's state digest, and one
|
|
345
|
+
authorization server's client registration, tokens, and discovery. Each
|
|
346
|
+
consent has a **flow record**, `oauth:flow:<sha256(state)>`, holding its
|
|
347
|
+
epoch, consent URL, and PKCE verifier for fifteen minutes; the state itself is
|
|
348
|
+
stored nowhere. Keys come from `src/storage/keys.ts`.
|
|
349
|
+
|
|
350
|
+
With a vault, the grant's body and each verifier are sealed with its AES-GCM
|
|
351
|
+
key, the additional authenticated data naming the connector, owner partition,
|
|
352
|
+
record, and epoch, so ciphertext moved to another connector, principal,
|
|
353
|
+
record, or epoch does not open. Anything that fails to open (tampered, or
|
|
354
|
+
under a rotated key) reads as absent: `auth_required` and a logged warning.
|
|
355
|
+
The epoch, consent URL, and state digest stay plaintext and authenticate
|
|
356
|
+
nothing. A plaintext body written before the vault could seal is sealed where
|
|
357
|
+
it lies on first read, by compare-and-set. An [`api()` OAuth
|
|
358
|
+
connector](#downstream-oauth-on-api) stores no client. A vault without the
|
|
359
|
+
optional `seal`/`open` members keeps the body plaintext with a startup
|
|
360
|
+
warning; no vault, plaintext as always.
|
|
361
|
+
|
|
362
|
+
**From 0.28 (layout 2).** The first read of an owner's grant that finds no
|
|
363
|
+
record reads layout 2 once: the active generation's issuer-stamped client,
|
|
364
|
+
tokens, and discovery (sealed values open under their old keys), kept only
|
|
365
|
+
when every credential carries a stamp and the stamps agree with each other
|
|
366
|
+
and with discovery. It writes them as the grant record by compare-and-set,
|
|
367
|
+
then deletes every layout-2 key, cleanup lineage included. A disconnected
|
|
368
|
+
connector stays disconnected; a cancelled request migrates nothing. Grants
|
|
369
|
+
from before issuer binding (v0.8.1 and earlier) and pending consents are not
|
|
370
|
+
carried, so those connectors need Connect again. Rolling back past this
|
|
371
|
+
release finds no layout-2 grant and needs consent.
|
|
372
|
+
|
|
373
|
+
## Starting, restarting, and disconnecting
|
|
374
|
+
|
|
375
|
+
Every grant write (registration, discovery, tokens, a refresh, a consent's
|
|
376
|
+
pointer, an invalidation) reads the record and replaces it by compare-and-set
|
|
377
|
+
against exactly what it read, so no write overwrites one it did not see.
|
|
378
|
+
Every flow (a connect attempt, an `api()` call or start, a callback) binds to
|
|
379
|
+
the epoch it began in. Once another epoch is live its reads fail, its writes
|
|
380
|
+
land nowhere, and it fails retryable `unavailable`: "authorization changed
|
|
381
|
+
while this request was in flight; try again". Restart and Disconnect replace
|
|
382
|
+
the epoch, and the grant with it, in one compare-and-set, so no older
|
|
383
|
+
namespace is left to clean up. They then delete each consent whose epoch is no
|
|
384
|
+
longer live; one left behind cannot complete and expires with its link.
|
|
385
|
+
`POST /ui/oauth/<id>` takes a `mode`:
|
|
386
|
+
|
|
387
|
+
| Request | What it does |
|
|
388
|
+
| ---------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
389
|
+
| `POST /ui/oauth/<id>` or `?mode=restart` | Issues a signed `/connect/<id>` link requesting a fresh epoch. No downstream authorization starts until the verified browser visits it. |
|
|
390
|
+
| `?mode=continue` | Issues a signed `/connect/<id>` link requesting continuation. At the browser visit, a recent pending consent can be reused; otherwise authorization begins in the current epoch. |
|
|
391
|
+
| `GET /connect/<id>?h=...` | Verifies the signed handoff and, when signed in, browser identity, connector visibility and management permission. With the UI mounted, hands off to `/connectors/<id>?h=...#auth` without consuming the link or starting OAuth; Clerk sign-in uses that shell. The Auth tab continues with `start=1`, which repeats verification, consumes the link and calls `startAuth`. Without the UI, the original browser visit starts consent. Explicit `POST /ui/oauth/<id>` actions return a signed link with `start=1` when the UI is mounted. |
|
|
392
|
+
| `DELETE /ui/oauth/<id>` | Disconnects and invalidates the cached catalog, even if the browser leaves. |
|
|
393
|
+
|
|
394
|
+
`authorize_connector` issues the same browser link, with `force` carried in the
|
|
395
|
+
signed handoff. On hosts without URL elicitation, direct and program auth
|
|
396
|
+
failures and search/describe catalog failures include the same authorization
|
|
397
|
+
handoff. The admitted registry and management permission govern issuance.
|
|
398
|
+
Each call or program run shares at most one handoff per connector; errors never
|
|
399
|
+
request a forced restart. Signed links retain their fifteen-minute expiry,
|
|
400
|
+
initiating-user and owner binding, origin check, and single-use browser nonce.
|
|
401
|
+
Minting a link stores no nonce and starts no consent. When an auth error includes
|
|
402
|
+
a handoff, give it to the user or operator. Status reads never call `startAuth` and never expose the
|
|
403
|
+
provider's consent URL. A UI start answers `{ state: "auth_required",
|
|
404
|
+
authorizationUrl }`, where the URL belongs to connecta. Continue hands back the
|
|
405
|
+
latest consent only while it is unclaimed, in the live epoch, written within
|
|
406
|
+
ten minutes, and names the grant's client. The cached catalog stays only when
|
|
407
|
+
an unforced browser visit reuses that consent or finds the connection
|
|
408
|
+
healthy. Restart invalidates it even if the connection ends healthy. Continue
|
|
409
|
+
trusts the stored registration's `redirect_uris`; after changing `publicUrl`,
|
|
410
|
+
Restart registers for the current callback URL.
|
|
411
|
+
|
|
412
|
+
### Consents and callbacks
|
|
413
|
+
|
|
414
|
+
A start stores its flow record, then the grant's pointer to it. A reset that
|
|
415
|
+
replaced the epoch in between fails the start and removes the record, so no
|
|
416
|
+
start hands out a consent its callback could not complete. The callback finds
|
|
417
|
+
its consent by the state's digest, one read whether or not it exists. The
|
|
418
|
+
exchange then:
|
|
419
|
+
|
|
420
|
+
1. binds to the consent's epoch and its issuer, client ID, token endpoint, and
|
|
421
|
+
discovery digest, noting the client and tokens the grant holds. A supplied
|
|
422
|
+
RFC 9207 `iss` must equal the consent's issuer;
|
|
423
|
+
2. after every read it depends on, claims the consent by compare-and-set from
|
|
424
|
+
the exact record found to a claimed one keeping neither URL nor verifier.
|
|
425
|
+
Of duplicate callbacks exactly one wins; the rest send nothing and get the
|
|
426
|
+
flat 400 for an already-used link;
|
|
427
|
+
3. after the claim, opens the current grant and checks its issuer, client ID,
|
|
428
|
+
token endpoint, and discovery against the consent. It re-reads that exact
|
|
429
|
+
record after opening ciphertext and sends the code in the reaction to that
|
|
430
|
+
read. Changing issuers advances the epoch as well. A
|
|
431
|
+
reset published before the read fails the callback with nothing sent. One
|
|
432
|
+
published while the request is leaving cannot be ordered before the send
|
|
433
|
+
without a lock across requests; the grant's compare-and-set refuses its
|
|
434
|
+
tokens, which costs one more consent.
|
|
435
|
+
|
|
436
|
+
The claim is spent whatever the exchange's outcome. The SDK's one retry after
|
|
437
|
+
a refused code receives that refusal and sends nothing. A refused code
|
|
438
|
+
invalidates the client or tokens only as step 1 found them, never what another
|
|
439
|
+
flow wrote meanwhile. Consumption is recorded on that consent alone, so a
|
|
440
|
+
delayed duplicate never deletes or invalidates a newer consent Continue
|
|
441
|
+
published. An exchange no callback verified claims only the consent its
|
|
442
|
+
callback's `state` names: programmatic `finishAuth`, or a PKCE-less `api()`
|
|
443
|
+
exchange. Both built-in `finishAuth` adapters require `callbackParams` with a
|
|
444
|
+
nonempty `state`, even with PKCE disabled or after a separate `verifyState` call.
|
|
445
|
+
A missing state is refused before discovery, registration, or token dispatch.
|
|
446
|
+
|
|
447
|
+
Layout-2 migration commits `cleanupPending` in the new grant before deleting
|
|
448
|
+
historical keys. Every later grant read retries deletion, then clears the
|
|
449
|
+
marker by CAS only when deletion succeeds. Restart preserves this obligation.
|
|
450
|
+
Disconnect commits a tombstone with the marker and always removes historical
|
|
451
|
+
keys for the connector and owner, including when a modern grant already exists.
|
|
452
|
+
A crash during cleanup cannot restore the disconnected grant.
|
|
453
|
+
|
|
454
|
+
Disconnect first removes the grant and fences its epoch with CAS. When that
|
|
455
|
+
removed grant's saved authorization-server metadata advertises a revocation
|
|
456
|
+
endpoint, Connecta makes one RFC 7009 request using that same issuer and client.
|
|
457
|
+
It revokes the refresh token when present, otherwise the access token. It never
|
|
458
|
+
rediscovers an issuer or sends credentials to a replacement server on Disconnect.
|
|
459
|
+
The endpoint must use HTTPS, with HTTP allowed only for the existing loopback
|
|
460
|
+
development exception. Trusting a configured HTTP MCP origin does not permit
|
|
461
|
+
plaintext public revocation. The advertised-URL destination guard also applies,
|
|
462
|
+
and revocation never follows redirects. The request has its own twenty-second deadline; browser
|
|
463
|
+
cancellation cannot undo local removal. It reads no provider response text.
|
|
464
|
+
|
|
465
|
+
A revocation refusal, timeout, network failure, or unsafe advertised destination
|
|
466
|
+
leaves the local grant removed. The management route returns HTTP 200 with
|
|
467
|
+
`{ state: "auth_required", code: "oauth_revocation_failed" }` and logs that typed
|
|
468
|
+
code. Revoke the old grant in the provider's console if removal there matters.
|
|
469
|
+
With no advertised endpoint, Disconnect makes no request. Repeating Disconnect
|
|
470
|
+
never retries revocation, because the removed grant is no longer available.
|
|
471
|
+
Restart makes no revocation request. Static `api()` OAuth has no discovered
|
|
472
|
+
revocation endpoint and continues to remove authorization locally.
|
|
399
473
|
|
|
400
474
|
## Refresh failures
|
|
401
475
|
|
|
402
|
-
A
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
The
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
476
|
+
A refresh token is spent when dispatch begins. Every refresh request passes
|
|
477
|
+
through `KvOAuthProvider.dispatchRefresh`. Before sending, the gate wins the
|
|
478
|
+
lease's `claimed` to `dispatched` CAS and a separate fingerprint CAS at
|
|
479
|
+
`oauth:refresh-spent:<sha256(refresh_token)>`. The latter must succeed before
|
|
480
|
+
any HTTP request leaves. Connector and owner storage namespaces partition both
|
|
481
|
+
records. Spent records have **no TTL**, contain no token, and are never deleted
|
|
482
|
+
by completion, failure, migration cleanup, Restart, Disconnect, or epoch sweeps.
|
|
483
|
+
Copy these durable records during storage migration.
|
|
484
|
+
|
|
485
|
+
Each fingerprint record names its dispatch epoch, holder, and resolution state:
|
|
486
|
+
|
|
487
|
+
| State | Send gate |
|
|
488
|
+
| ------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
489
|
+
| `outstanding` | Dispatched without a definitive outcome yet. Refuse the fingerprint in every epoch. |
|
|
490
|
+
| `ambiguous` | The sent request's outcome is unknowable. Refuse the fingerprint in every epoch, including when re-consent returns it again. |
|
|
491
|
+
| `resolved` | The tokens were committed or a definitive failure was recorded. Refuse another send in the same epoch. A later epoch may send once only when its code exchange completed after the resolution was recorded. |
|
|
492
|
+
|
|
493
|
+
Resolution writes use CAS against the exact outstanding record and record a
|
|
494
|
+
`resolvedAt` timestamp with a resolved outcome. When accepting a code response,
|
|
495
|
+
the coordinator reads the returned refresh token's fingerprint record. Consent
|
|
496
|
+
stores the exact resolved record it observed with the grant's tokens. That
|
|
497
|
+
receipt proves resolution preceded consent completion without comparing
|
|
498
|
+
isolate clocks. A delayed SDK save cannot acquire a newer receipt. The send
|
|
499
|
+
CAS compares against that same resolved record and replaces it with the new
|
|
500
|
+
outstanding dispatch. An earlier consent cannot become eligible merely because
|
|
501
|
+
the pending refresh later resolves. Competing epochs cannot both replace the
|
|
502
|
+
same record, and a late answer cannot turn an ambiguous record into resolved.
|
|
503
|
+
|
|
504
|
+
An ambiguous token stays blocked even if re-consent returns identical bytes.
|
|
505
|
+
Recovery requires re-consent that yields a different refresh token, or
|
|
506
|
+
revocation at the provider. Restart never deletes spent records or moves old
|
|
507
|
+
credentials into a new epoch.
|
|
508
|
+
|
|
509
|
+
Credential-bearing token-endpoint requests never follow redirects, regardless
|
|
510
|
+
of `remoteMcp()`'s `redirects` setting. The send gate uses `redirect: "manual"`
|
|
511
|
+
and bypasses the resource redirect wrapper for refresh, authorization-code,
|
|
512
|
+
and client-credentials grants and token revocation. Static `api()` OAuth uses
|
|
513
|
+
the same gate and manual fetch. Any 3xx is a definitive failure, even if its
|
|
514
|
+
body contains tokens. Fetch has already sent the request body; a refresh
|
|
515
|
+
fingerprint is resolved as a definitive failure and the grant requires re-consent. A
|
|
516
|
+
redirected code exchange is refused and its SDK retry cannot resend the code.
|
|
517
|
+
|
|
518
|
+
| Outcome after dispatch | Result |
|
|
519
|
+
| ------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
520
|
+
| Valid tokens durably committed | Mark the fingerprint resolved and release waiters with committed tokens. Within the epoch only a new fingerprint can be dispatched next. |
|
|
521
|
+
| Definitive provider failure | `auth_required`. Mark the fingerprint resolved and conditionally remove its grant tokens. Includes every 3xx without following it and complete failures such as 5xx, 408, 425, and 429. |
|
|
522
|
+
| Network or response-body loss, malformed or oversized success, deadline expiry, or process crash | `auth_required`. Mark the fingerprint ambiguous and conditionally remove its grant tokens. Identical-token re-consent cannot reopen it. |
|
|
523
|
+
| Valid rotation whose grant commit retries are exhausted | `auth_required`. Record the definitive commit failure as resolved and conditionally remove its grant tokens. Never return uncommitted tokens to the SDK. |
|
|
524
|
+
| Epoch changed during commit | Drop the response tokens and record the definitive failure as resolved. Restart, Disconnect, or issuer replacement determines the newer grant. A consent that completed before this resolution cannot reuse its fingerprint. |
|
|
525
|
+
|
|
526
|
+
Refreshes coalesce per owner and epoch within a runtime. Across isolates, a
|
|
527
|
+
shared-storage record at `oauth:refresh:<epoch>:<sha256(refresh_token)>` is
|
|
528
|
+
claimed by CAS before dispatch. Only an expired unsent claim can be taken
|
|
529
|
+
over. Its old holder loses the dispatch CAS. A dispatched record cannot be
|
|
530
|
+
reopened by a completed answer, a verdict, or a newcomer read.
|
|
531
|
+
|
|
532
|
+
The separate holder record at `oauth:refresh-active:<epoch>:<holder>` has a
|
|
533
|
+
storage-owned 120-second TTL. SQLite and D1 create and check expiry with the
|
|
534
|
+
database clock inside each statement. Custom shared stores must also use a
|
|
535
|
+
storage-owned clock. Cross-isolate clock comparisons never permit takeover of
|
|
536
|
+
a dispatched record. The refresh HTTP request and response body have a
|
|
537
|
+
20-second deadline. Contenders wait at most 35 seconds with 10 ms to 250 ms
|
|
538
|
+
backoff. A waiter deadline returns retryable `unavailable`, keeps the grant,
|
|
539
|
+
and starts no consent. It never permits resending a spent token.
|
|
540
|
+
|
|
541
|
+
One preparation deadline covers initial epoch and grant reads, claim acquisition,
|
|
542
|
+
dispatch preparation, and local joiners waiting for a grant commit. Cancellation
|
|
543
|
+
before sending settles the local flight without awaiting cleanup storage. An
|
|
544
|
+
unsent claim can expire and be taken over. Late preparation never sends. If
|
|
545
|
+
preparation recorded the fingerprint as spent, it stays spent even if the send
|
|
546
|
+
was subsequently cancelled or a local destination guard refused it.
|
|
547
|
+
|
|
548
|
+
After dispatch, caller and SDK cancellation end only that caller's wait. The
|
|
549
|
+
exchange and grant commit continue under the HTTP deadline. The runtime passes
|
|
550
|
+
completion to Workers `waitUntil`; paths without that hook keep a background
|
|
551
|
+
promise. The holder re-reads the grant and retries the commit with its in-memory
|
|
552
|
+
response tokens up to 32 times after CAS contention or storage errors. Each
|
|
553
|
+
attempt checks the epoch, issuer, and current refresh token. A changed epoch
|
|
554
|
+
drops the response tokens. Another consent's credentials remain intact. A lost
|
|
555
|
+
commit answer can be recovered by reading the committed grant. Exhausting the
|
|
556
|
+
commit retries requires re-consent.
|
|
557
|
+
|
|
558
|
+
Lease release uses CAS after the commit or re-consent decision. Completion
|
|
559
|
+
records include a SHA-256 digest of the committed token response, excluding
|
|
560
|
+
the local issuer stamp. Waiters can adopt that committed response even when
|
|
561
|
+
it kept the refresh token or was byte-identical. A newcomer cannot clear the
|
|
562
|
+
completion record to redeem that fingerprint again. Verdict records contain
|
|
563
|
+
fixed typed facts, without tokens or downstream text.
|
|
564
|
+
|
|
565
|
+
If a dispatched holder crashes, shared-storage liveness expiry records
|
|
566
|
+
re-consent and conditionally removes its grant tokens. A late response cannot
|
|
567
|
+
restore them. Spent records already prohibit replay, including when refusal
|
|
568
|
+
recording or token cleanup fails. Restart and Disconnect sweep obsolete lease
|
|
569
|
+
records while retaining every spent record. Holder liveness remains until
|
|
570
|
+
completion or storage-owned TTL expiry, so reset cannot turn a live request
|
|
571
|
+
into an ambiguous one. The SDK receives
|
|
572
|
+
sanitized failure responses, and provider hooks preserve the re-consent verdict
|
|
573
|
+
for passive calls. A source-level guard pins both OAuth adapters to this single
|
|
574
|
+
send gate, manual token fetches, and a separate resource redirect path.
|
|
575
|
+
Connector status reports `auth_required`; agent calls report
|
|
576
|
+
`downstream_oauth_required` with the authorization recovery action. A 403
|
|
577
|
+
during a dispatched refresh also requires re-consent because its fingerprint
|
|
578
|
+
is already spent. Provider permission denials on other calls retain their
|
|
579
|
+
`provider_permission_denied` recovery.
|
|
580
|
+
|
|
581
|
+
Refresh and failed code-exchange answers are rebuilt from the OAuth `error`
|
|
582
|
+
code alone with fixed text. The SDK logs descriptions below the configured
|
|
583
|
+
logger; downstream descriptions and storage errors can quote tokens, client
|
|
584
|
+
secrets, or codes and must not reach that output.
|
|
585
|
+
|
|
586
|
+
## What a server's errors may say
|
|
587
|
+
|
|
588
|
+
No authorization, token, registration, or discovery server's text reaches an
|
|
589
|
+
agent: it can echo the request or plant words for an agent. No server's text
|
|
590
|
+
reaches a log, status message, or activity row at all
|
|
591
|
+
([records](./architecture.md#errors-and-records)). A failed
|
|
592
|
+
OAuth flow names its step, the host of that flow's own last request (each flow
|
|
593
|
+
keeps its own, so concurrent calls never borrow one), and for a refused
|
|
594
|
+
registration the status and a registered OAuth `error` code. Google's delegated
|
|
595
|
+
token endpoint gets the same treatment. At the MCP handshake, `tools/list`, and
|
|
596
|
+
`tools/call`, an error keeps its text only if it is the downstream's JSON-RPC
|
|
597
|
+
error answer, the endpoint's HTTP 4xx refusal, or the request's own abort reason
|
|
598
|
+
(checked first, by identity or as the SDK rewraps it). Anything else, a parser's,
|
|
599
|
+
validator's, runtime's, or non-4xx body's account included, becomes the step,
|
|
600
|
+
host, HTTP status if any, and error class. An `api()` handler's failure other
|
|
601
|
+
than a `ConnectorCallError` names its tool and class, and an argument mismatch
|
|
602
|
+
is told from the reviewed findings, never the validator's sentence. Every
|
|
603
|
+
withheld error keeps its original classification, and no connector error keeps
|
|
604
|
+
a runtime, stream, or parser error as `cause`.
|
|
605
|
+
|
|
606
|
+
The JSON-RPC and 4xx exception preserves diagnostics agents need to correct
|
|
607
|
+
their arguments. Core redacts credentials and submitted `writeOnly` argument values before these
|
|
608
|
+
messages, nested causes/data, or tool results reach an agent or guest program.
|
|
609
|
+
The memory-only set covers credential-slot values, static auth headers,
|
|
610
|
+
outbound bearer tokens, and their JSON-escaped, URL-encoded, base64 and
|
|
611
|
+
base64url forms. Final outgoing requests register Authorization,
|
|
612
|
+
Proxy-Authorization, Cookie, and headers or query parameters whose names
|
|
613
|
+
contain `key`, `token`, `secret`, `auth`, `signature`, or `session`. Custom
|
|
614
|
+
`api()` handlers use `ctx.fetch` for this tracking; `ctx.oauth.fetch` and
|
|
615
|
+
maintained-provider transports track the final request too. Credential-slot
|
|
616
|
+
reads cover values sent in headers, queries, or bodies. Echoed sensitive
|
|
617
|
+
header lines are also withheld.
|
|
618
|
+
|
|
619
|
+
Submitted values under JSON Schema `writeOnly: true` join this memory-only
|
|
620
|
+
request set, including nested objects, arrays, and local references. Strings
|
|
621
|
+
and numbers register with the same raw, URL, form, base64, and base64url forms
|
|
622
|
+
as credentials; booleans and null carry no text. Property names inside a
|
|
623
|
+
private object subtree are private values too; array indices are not. Output
|
|
624
|
+
redaction protects the union of private fields across `oneOf`/`anyOf`
|
|
625
|
+
alternatives. If a schema declares private fields but its sensitivity cannot
|
|
626
|
+
be resolved, all submitted values are treated as private. Schemas without a
|
|
627
|
+
`writeOnly` annotation skip this work. Private values never enter stored
|
|
628
|
+
catalogs or operator records. The program's outgoing-argument filter and
|
|
629
|
+
catalog caches use credentials alone, so later calls receive the original
|
|
630
|
+
submitted data and cached catalogs never depend on one request's arguments.
|
|
631
|
+
|
|
632
|
+
Payload rules apply where downstream or guest data enters: downstream results
|
|
633
|
+
and errors, resource reads, connector usage guides and downstream skill files,
|
|
634
|
+
program returns, logs, and emits, and relayed input prompts. JSON objects,
|
|
635
|
+
arrays, and string literals are parsed and walked, whether they are wire text
|
|
636
|
+
or a string inside a result, so nested JSON strings are decoded too. Every
|
|
637
|
+
decoded string leaf and property name is scanned, and a number, boolean, or
|
|
638
|
+
null whose canonical text matches becomes the string `[redacted]`, so the JSON
|
|
639
|
+
stays valid. Text is reserialized
|
|
640
|
+
only when something changed; otherwise its original bytes remain. The
|
|
641
|
+
serialized bytes are then scanned raw, and a literal the walk cannot see, such
|
|
642
|
+
as one spanning tokens, withholds the whole text. Other text is scanned raw and
|
|
643
|
+
through one more layer of JSON string escapes at a time while the last layer
|
|
644
|
+
could still hold an escape. A string is decoded through at most eight layers in
|
|
645
|
+
total, counting the nested JSON strings it was parsed from; text that would
|
|
646
|
+
need more is withheld. Every match is mapped back to the original text,
|
|
647
|
+
overlapping and adjacent spans merge, and each merged span is replaced once, so
|
|
648
|
+
one match can never split another. A span wholly inside an existing
|
|
649
|
+
`[redacted]` placeholder is that placeholder; a private value that contains the
|
|
650
|
+
placeholder text is still redacted. Connecta and MCP envelope field names, tag
|
|
651
|
+
values such as a content `type` or result `format`, and typed facts such as
|
|
652
|
+
`isError` and `retryable` are copied unchanged. The agent boundary around
|
|
653
|
+
Connecta's own envelopes repeats credential and literal private-value
|
|
654
|
+
replacement without the short-value rules, so envelope identifiers, counts, and
|
|
655
|
+
JSON-RPC ids are never rewritten.
|
|
656
|
+
|
|
657
|
+
Empty private strings are exempt because they contain nothing to leak. A
|
|
658
|
+
non-empty private value shorter than eight characters redacts a structured
|
|
659
|
+
string leaf, property name, or scalar only when it equals the value or a
|
|
660
|
+
supported encoded form, including after JSON unescaping. A longer structured
|
|
661
|
+
field that merely contains it, such as `prod-db` for `prod`, is kept: this
|
|
662
|
+
documented tradeoff keeps identifiers and approval state usable. Prose that
|
|
663
|
+
contains a short value or encoded form anywhere is withheld as `[redacted]`:
|
|
664
|
+
plain-text results, `message`, `error`, `text`, `logs`, and `stack` strings,
|
|
665
|
+
string program results, and relayed prompts. Typed error classification,
|
|
666
|
+
retryability, retry delay, and write-outcome accounting remain. Private values
|
|
667
|
+
of at least eight characters are replaced wherever they occur.
|
|
668
|
+
|
|
669
|
+
Matching runs one Aho-Corasick automaton over every registered form, so each
|
|
670
|
+
scanned code unit costs amortized constant work however many values are
|
|
671
|
+
registered. The automaton indexes the first 256 code units of each form,
|
|
672
|
+
credentials first, and at most 512 Ki indexed units for every registration
|
|
673
|
+
together, so a long credential or private value costs no more automaton memory
|
|
674
|
+
than its prefix. Where a longer form's prefix matches, the scanned view's
|
|
675
|
+
rolling hash confirms the rest once its last unit arrives; equal text always
|
|
676
|
+
confirms, so a hash collision could only redact more. Forms that share a prefix
|
|
677
|
+
and a length share each confirmation, and overlapping prefix occurrences in
|
|
678
|
+
self-similar text share one run of confirmations, so periodic text stays exact.
|
|
679
|
+
Each confirmation is charged to the call's work budget before it is queued, and
|
|
680
|
+
one scanned view holds at most 16 Ki runs; a view that needs more is withheld.
|
|
681
|
+
Submitted values register at most 512 KiB of encoded forms and 4,096
|
|
682
|
+
argument nodes. A form that does not fit either limit withholds every string
|
|
683
|
+
at least as long as that form; an argument tree that cannot be walked withholds
|
|
684
|
+
all downstream text.
|
|
685
|
+
|
|
686
|
+
One redaction call has one work budget across parsing and every scanned view:
|
|
687
|
+
64 Mi units with private values, and 1 Gi for credentials alone, which keeps
|
|
688
|
+
multi-MiB document and skill-file reads. Work that does not fit the remaining
|
|
689
|
+
budget withholds its field, so smaller identifiers can still be checked. The
|
|
690
|
+
same call walks structured values iteratively, at most 512 containers deep and
|
|
691
|
+
1 Mi entries in total, and copies only containers on a path that changed. A
|
|
692
|
+
container's entries are counted before it is walked. One that does not fit the
|
|
693
|
+
remaining entries is scanned as serialized text instead: kept unchanged when
|
|
694
|
+
nothing registered appears, otherwise withheld. A cycle or a container past the
|
|
695
|
+
depth limit is withheld. No limit throws or exposes a matcher diagnostic, and
|
|
696
|
+
typed success outcomes remain.
|
|
697
|
+
|
|
698
|
+
Only credentials of at least eight characters enter the redaction and structural
|
|
699
|
+
matchers. Credentials shorter than 8 characters are not redacted from echoes;
|
|
700
|
+
use longer secrets. Matching short secrets can corrupt JSON-RPC fields or refuse
|
|
701
|
+
legitimate OAuth metadata. A configured static secret or Basic password below
|
|
702
|
+
this floor produces one operator warning per connector with the typed code
|
|
703
|
+
`short_secret_not_redacted` and no credential value. Remote connectors warn at
|
|
704
|
+
construction; static `api()` OAuth warns on its first provider operation.
|
|
705
|
+
Connecta's own messages never quote credential values, regardless of length.
|
|
706
|
+
One automaton is cached until the secret set changes, and an empty set skips matching. Redaction runs after
|
|
707
|
+
JSON unwrapping or joining text blocks and on final serialized text and every
|
|
708
|
+
structured string, before result paging, emits, and program outputs/errors/logs. See
|
|
709
|
+
[the agent boundary](./architecture.md#errors-and-records).
|
|
425
710
|
|
|
426
711
|
## URLs a downstream advertises
|
|
427
712
|
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
endpoints the authorization server publishes. Connecta fetches every one of
|
|
432
|
-
them server-side, so a compromised downstream could otherwise aim a Node or
|
|
433
|
-
Docker host at its own network — cloud metadata at `169.254.169.254`, an admin
|
|
434
|
-
panel on the LAN. Config is the security model, so the rule splits on it:
|
|
713
|
+
`remoteMcp()` learns server-side fetch URLs from a 401's `resource_metadata`,
|
|
714
|
+
its `authorization_servers`, and advertised token and registration endpoints.
|
|
715
|
+
To prevent a downstream from targeting the host's private network:
|
|
435
716
|
|
|
436
|
-
- A URL on the connector's configured origin is trusted
|
|
437
|
-
down.
|
|
717
|
+
- A URL on the connector's configured origin is trusted; the operator wrote it.
|
|
438
718
|
- Any other URL must be `https` and must not name a private host: `localhost`
|
|
439
719
|
and `*.localhost`, `127/8`, `10/8`, `172.16/12`, `192.168/16`, `169.254/16`,
|
|
440
720
|
`100.64/10`, `0/8`, `::`, `::1`, `fc00::/7`, `fe80::/10`, or an IPv4-mapped
|
|
@@ -444,39 +724,75 @@ panel on the LAN. Config is the security model, so the rule splits on it:
|
|
|
444
724
|
working. LAN and link-local addresses stay refused.
|
|
445
725
|
|
|
446
726
|
A refused URL is never requested. Discovery, registration, and code exchange
|
|
447
|
-
fail with a non-retryable `connector_call_failed`
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
hops.
|
|
453
|
-
|
|
454
|
-
The check is syntactic
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
727
|
+
fail with a non-retryable `connector_call_failed` naming the host and nothing
|
|
728
|
+
else from the URL. A refused refresh never reaches the token endpoint. If the
|
|
729
|
+
send gate already recorded its fingerprint, that fingerprint remains blocked
|
|
730
|
+
and the grant requires re-consent. Redirects cannot route around the
|
|
731
|
+
rule; token requests never follow them, and resource redirects follow only
|
|
732
|
+
same-origin hops.
|
|
733
|
+
|
|
734
|
+
The check is syntactic: it reads the host after the WHATWG URL parser folds
|
|
735
|
+
`2130706433` and `0x7f.1` into `127.0.0.1`, and never resolves a name — the
|
|
736
|
+
Workers-safe core has no DNS. A public name resolving or rebinding to a private
|
|
737
|
+
address is out of scope; a host that must stop that needs an egress policy.
|
|
738
|
+
|
|
739
|
+
Downstream advertisements choose consent destinations, never destinations for
|
|
740
|
+
existing grants. Tokens and registered clients bind to their issuing server;
|
|
741
|
+
cached discovery sends refresh there despite changed advertisements. Fresh
|
|
742
|
+
discovery naming another issuer replaces the grant's body (see [one server per
|
|
743
|
+
grant](#one-server-per-grant)), so it receives no existing credentials and
|
|
744
|
+
registers and consents anew. The SDK
|
|
745
|
+
also refuses [GHSA-6qxp-vccf-f47h](https://github.com/advisories/GHSA-6qxp-vccf-f47h)
|
|
746
|
+
since client 2.2.0. New consent still follows the downstream's URL; the human
|
|
747
|
+
must read it before approving.
|
|
748
|
+
|
|
749
|
+
Grants from before issuer binding (v0.8.1 and earlier) have no stamp and are
|
|
750
|
+
not migrated (see [state at rest](#downstream-oauth-state-at-rest)).
|
|
751
|
+
|
|
752
|
+
### One server per grant
|
|
753
|
+
|
|
754
|
+
A grant record names one authorization server, its `issuer`. Saving
|
|
755
|
+
discovery, a client, or tokens for another server replaces the body, and an
|
|
756
|
+
issuer-aware read hands a value only to the server that issued it. The SDK
|
|
757
|
+
discovers afresh only without cached discovery, after a forced restart's
|
|
758
|
+
carried client for example; a different server found there is handed nothing
|
|
759
|
+
and registers anew. Nothing is decided at flow entry and no hook retires
|
|
760
|
+
anything, so SDK hook order cannot mix servers or cross a reset.
|
|
459
761
|
|
|
460
762
|
## Management permissions
|
|
461
763
|
|
|
462
|
-
Visibility
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
`
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
unknown ids fail closed, and permissions come from authenticated identity, never
|
|
471
|
-
from caller input.
|
|
764
|
+
Visibility grants no authentication-management permission. Two resolvers take
|
|
765
|
+
`Readonly<AuthenticatedIdentity>` and return `"all"`, `"none"` (the default),
|
|
766
|
+
or connector ids: `credentialAdministration` for shared credentials and OAuth
|
|
767
|
+
grants, `personalConnection` for a human's own grants on personal connectors.
|
|
768
|
+
Each action needs visibility _and_ the permission; both run only for an
|
|
769
|
+
interactive identity, and personal actions also need a stable namespaced
|
|
770
|
+
principal, whose partition they always use. Exceptions and unknown ids fail
|
|
771
|
+
closed; permissions come from authenticated identity, never caller input.
|
|
472
772
|
|
|
473
773
|
`identity.activityAccess` takes `Readonly<IdentityReference>` — `id` and
|
|
474
|
-
`namespace` — and controls reading
|
|
774
|
+
`namespace` — and controls reading activity. When configured, an authenticated
|
|
775
|
+
principal may pass it without an interactive session. This includes a `cta_`
|
|
776
|
+
token's stored principal and a non-interactive API principal; subject-only
|
|
777
|
+
callers and anonymous requests cannot qualify. The optional activity `readGate`
|
|
778
|
+
also applies. Reads remain GET-only and filter connector/tool grants, personal
|
|
779
|
+
ownership and recorded pool access before any actor-label lookup. Personal rows
|
|
780
|
+
use the admitted principal in the existing typed actor id and namespace fields;
|
|
781
|
+
rows without a provable owner are withheld. Non-interactive
|
|
782
|
+
reads return typed actors without directory labels. Old rows without a pool
|
|
783
|
+
remain subject to connector, tool and owner checks. Undeclared, it admits every
|
|
475
784
|
interactive human, the one default here that is open, because a single-operator
|
|
476
785
|
deployment would otherwise be locked out of its own event stream. Team
|
|
477
786
|
deployments should set it. There is no general administrator role and no
|
|
478
787
|
implicit token-management authority. `identity.accessTokenManagement` is a separate boolean permission, false by default, evaluated only for interactive humans. Lifecycle routes also require a stable principal.
|
|
479
788
|
|
|
789
|
+
Personal activity rows must carry the checked `principal` actor basis as well as
|
|
790
|
+
a matching principal ID and namespace. Legacy subject actors cannot prove
|
|
791
|
+
personal ownership and are withheld. Activity pagination advances past hidden
|
|
792
|
+
boundary rows so their timestamp and event ID never appear in a returned cursor.
|
|
793
|
+
Each read belongs to the request fiber. A scan exceeding 1,000 additional
|
|
794
|
+
boundary reads returns 503 without a cursor; cancellation starts no further read.
|
|
795
|
+
|
|
480
796
|
```ts
|
|
481
797
|
createConnecta({
|
|
482
798
|
auth: cloudflareAccessAuth(),
|
|
@@ -492,42 +808,36 @@ createConnecta({
|
|
|
492
808
|
personalConnection: () => ["personal_linear"],
|
|
493
809
|
activityAccess: ({ id }) => id === "owner-id",
|
|
494
810
|
},
|
|
495
|
-
connectors,
|
|
496
|
-
executor,
|
|
811
|
+
connectors, executor,
|
|
497
812
|
});
|
|
498
813
|
```
|
|
499
814
|
|
|
500
815
|
## Cloudflare Access on Workers
|
|
501
816
|
|
|
502
817
|
[`cloudflareAccessAuth()`](https://developers.cloudflare.com/workers/configuration/cloudflare-access/)
|
|
503
|
-
is the Worker-specific path
|
|
504
|
-
|
|
505
|
-
|
|
818
|
+
is the supported Worker-specific path under
|
|
819
|
+
[#703](https://github.com/zackbart/connecta/issues/703). Node uses Clerk;
|
|
820
|
+
Access trusts the identity validated at the Worker edge.
|
|
506
821
|
|
|
507
822
|
```ts
|
|
508
|
-
import { cloudflareAccessAuth } from
|
|
509
|
-
"@zackbart/connecta/auth/cloudflare-access";
|
|
510
|
-
|
|
823
|
+
import { cloudflareAccessAuth } from "@zackbart/connecta/auth/cloudflare-access";
|
|
511
824
|
createConnecta({ auth: cloudflareAccessAuth(), connectors, executor });
|
|
512
825
|
```
|
|
513
826
|
|
|
514
|
-
The adapter trusts only `ctx.access`, which Cloudflare
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
`
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
Access decides admission and identity; connecta configuration decides connector
|
|
529
|
-
access and management permissions. An Access service identity, having no human
|
|
530
|
-
principal, cannot mutate connection auth at all.
|
|
827
|
+
The adapter trusts only `ctx.access`, which Cloudflare attaches after validating
|
|
828
|
+
the token against the Worker-level Access application's AUD. A nonempty trusted
|
|
829
|
+
`ctx.access.aud` is required; a caller cannot supply this context through HTTP.
|
|
830
|
+
`ctx.access.getIdentity()` supplies `user_uuid`, with `email` as fallback, in the
|
|
831
|
+
stable `cloudflare-access` principal namespace. Missing identity, service
|
|
832
|
+
identity, or unusable user ids refuse. Access service credentials can admit the
|
|
833
|
+
request at the edge, but connecta requires a `cta_` token for machine identity.
|
|
834
|
+
|
|
835
|
+
It never reads `Cf-Access-Jwt-Assertion`, fetches signing keys, or accepts a
|
|
836
|
+
caller's JWT. Missing context or a throwing lookup fails closed on both
|
|
837
|
+
runtimes. It does not support Node, a `cloudflared` origin, or a Service Binding
|
|
838
|
+
hop. [Decision 0003](https://github.com/zackbart/connecta/blob/main/decisions/0003-inbound-auth.md) records that boundary
|
|
839
|
+
and replaces #506's provisional verdict with supported Worker Access.
|
|
840
|
+
Connecta configuration still decides connector and management permissions.
|
|
531
841
|
|
|
532
842
|
Protect the Worker with a Worker-level Access application whose destination is
|
|
533
843
|
`{ "type": "worker", "worker_id": "<the Worker script tag>" }`. A traditional
|
|
@@ -545,126 +855,332 @@ https://chatgpt.com/connector_platform_oauth_redirect
|
|
|
545
855
|
https://chatgpt.com/connector/oauth/*
|
|
546
856
|
```
|
|
547
857
|
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
let discovery succeed and registration fail later, which looks like a broken MCP
|
|
555
|
-
server rather than a console setting.
|
|
858
|
+
That list is `oauth_configuration.dynamic_client_registration.allowed_uris`, on
|
|
859
|
+
the Managed OAuth settings, not the Access policy that picks identities. Claude
|
|
860
|
+
uses the first; ChatGPT its stable callback or a callback-id path the third
|
|
861
|
+
covers. For another client add its exact URI or the narrowest wildcard covering
|
|
862
|
+
it, never its whole origin. A missing entry lets discovery succeed and
|
|
863
|
+
registration fail later, which looks like a broken MCP server, not a setting.
|
|
556
864
|
|
|
557
865
|
Do not add a bypass for the discovery routes; a fully automated client uses a
|
|
558
866
|
[service token](https://developers.cloudflare.com/cloudflare-one/access-controls/service-credentials/service-tokens/)
|
|
559
867
|
through `CF-Access-Client-Id` and `CF-Access-Client-Secret` instead. Worker-level
|
|
560
868
|
Access runs before every connecta route, so `/health`, operator pages, downstream
|
|
561
869
|
OAuth callbacks, and `/mcp` all require Access unless a more-specific policy says
|
|
562
|
-
otherwise, and a
|
|
870
|
+
otherwise, and a `cta_` token is not a standalone edge credential
|
|
563
871
|
because Cloudflare rejects the request before connecta sees it. Custom public
|
|
564
872
|
webhooks live outside connecta and need their own Access routing policy. The
|
|
565
873
|
[Worker example](../examples/worker/) carries the whole deployment shape.
|
|
566
874
|
|
|
875
|
+
## Clerk OAuth tokens and operator sessions
|
|
876
|
+
|
|
877
|
+
On `/mcp` and `/mcp/<pool>`, `clerkAuth` requires resource-bound Clerk OAuth
|
|
878
|
+
tokens by default, both JWT and opaque. Verified `aud` or `resource` must
|
|
879
|
+
contain the endpoint's exact canonical URL from protected-resource metadata.
|
|
880
|
+
If both claims are present, both must match. Trailing slashes, case, queries,
|
|
881
|
+
and fragments are not normalized. `/mcp` and `/mcp/support` are distinct
|
|
882
|
+
resources. Set `publicUrl` behind an internal origin.
|
|
883
|
+
|
|
884
|
+
```ts
|
|
885
|
+
clerkAuth({ publishableKey, secretKey,
|
|
886
|
+
publicUrl: "https://connecta.example.com" });
|
|
887
|
+
```
|
|
888
|
+
|
|
889
|
+
### Enable resource audiences in Clerk
|
|
890
|
+
|
|
891
|
+
Enable `aud_claim_enabled: true` in Clerk's instance OAuth application
|
|
892
|
+
settings. The [Backend API contract](https://github.com/clerk/openapi-specs/blob/f10fb179f42fc591f9d6f0221c089dd19483fd69/bapi/2026-05-12.yml)
|
|
893
|
+
defines `GET` and `PATCH /v1/instance/oauth_application_settings` on
|
|
894
|
+
`https://api.clerk.com`. Patch the setting and read it back to confirm.
|
|
895
|
+
`oauth_jwt_access_tokens` separately selects JWT or opaque format; both work.
|
|
896
|
+
|
|
897
|
+
For `/mcp`, `/.well-known/oauth-protected-resource` advertises
|
|
898
|
+
`resource: "https://connecta.example.com/mcp"` and the Clerk Frontend API
|
|
899
|
+
origin in `authorization_servers`. Pool metadata lives at
|
|
900
|
+
`/.well-known/oauth-protected-resource/mcp/<pool>` and advertises that pool's
|
|
901
|
+
exact URL. Clients fetch authorization-server metadata directly from that Clerk origin.
|
|
902
|
+
Connecta serves no authorization-server metadata proxy.
|
|
903
|
+
|
|
904
|
+
MCP hosts send that URL as the RFC 8707 `resource` parameter in standard OAuth
|
|
905
|
+
authorization and token requests. Clerk's [Frontend API contract](https://github.com/clerk/openapi-specs/blob/f10fb179f42fc591f9d6f0221c089dd19483fd69/fapi/2026-05-12.yml)
|
|
906
|
+
supports it at authorization and token exchange. Exchange and refresh retain
|
|
907
|
+
the grant's resource when omitted; if supplied, it must match. Enable the
|
|
908
|
+
registration methods your hosts use, such as DCR or [Client ID Metadata Documents](https://clerk.com/docs/guides/configure/auth-strategies/oauth/client-id-metadata-documents).
|
|
909
|
+
Claude, ChatGPT, Claude Code, Codex, and Cursor should use their standard MCP
|
|
910
|
+
OAuth flow. Bound tokens require no connecta client-ID list.
|
|
911
|
+
|
|
912
|
+
When migrating, enable audience issuance, reconnect for a new resource-bound
|
|
913
|
+
grant, and verify:
|
|
914
|
+
|
|
915
|
+
1. The endpoint's protected-resource metadata advertises the exact public MCP
|
|
916
|
+
URL as `resource`, including any pool suffix.
|
|
917
|
+
2. The host sends that `resource` at authorization and exchange. Inspect the
|
|
918
|
+
new token locally: JWT claims or Clerk's opaque-token verification response
|
|
919
|
+
from `POST /v1/oauth_applications/access_tokens/verify` must contain that URL
|
|
920
|
+
in `aud`. Keep tokens out of logs and reports.
|
|
921
|
+
3. With `allowedOAuthClientIds` omitted or `[]`, MCP initialization succeeds at
|
|
922
|
+
that URL and a different endpoint's token receives `401`. Refresh preserves
|
|
923
|
+
the audience and operator sign-in works. Repeat for each host and pool.
|
|
924
|
+
|
|
925
|
+
Clerk's SDK verifies both formats. Connecta reads the authenticated JWT's
|
|
926
|
+
claims. SDK 3.12.0 drops opaque `aud`, so connecta makes one additional Backend
|
|
927
|
+
API verification request and reads the raw response. Its subject and client
|
|
928
|
+
must agree with the SDK, and it must report an unrevoked, unexpired token.
|
|
929
|
+
The extra call follows request cancellation, times out after ten seconds,
|
|
930
|
+
caches no token or audience, and denies admission on failure.
|
|
931
|
+
|
|
932
|
+
### Explicit fallback for unbound tokens
|
|
933
|
+
|
|
934
|
+
`allowedOAuthClientIds: ["your-connecta-client-id"]` admits verified tokens
|
|
935
|
+
with no audience/resource claim from exact client IDs dedicated to this
|
|
936
|
+
deployment. Never share these clients with another resource server. A present
|
|
937
|
+
malformed or mismatched binding is rejected even for an allowlisted client.
|
|
938
|
+
The list does not restrict correctly bound tokens. It applies to every pool;
|
|
939
|
+
identity and pool grants still narrow access. Profile scopes do not identify a
|
|
940
|
+
resource, and `scopes` remains metadata, not an admission rule.
|
|
941
|
+
|
|
942
|
+
Each new DCR registration gets a new client ID and needs an operator update
|
|
943
|
+
for this fallback. Hosts may cache registrations, but the list is impractical
|
|
944
|
+
for standard onboarding. A CIMD client identifies the host, not the deployment
|
|
945
|
+
resource. Prefer resource audiences with `allowedOAuthClientIds` omitted or `[]`.
|
|
946
|
+
|
|
947
|
+
Clerk session tokens authenticate operator routes and browser OAuth callbacks
|
|
948
|
+
only, with the deployment-origin `azp` check. MCP rejections retain the `401`
|
|
949
|
+
Bearer challenge and endpoint `resource_metadata` URL required by
|
|
950
|
+
[MCP authorization](https://modelcontextprotocol.io/specification/2026-07-28/basic/authorization#token-handling).
|
|
951
|
+
Authentication logs contain fixed codes such as `oauth_binding_mismatch`,
|
|
952
|
+
`oauth_client_not_allowed`, and `oauth_verification_failed`, never tokens,
|
|
953
|
+
client IDs, claimed audiences, or upstream error text.
|
|
954
|
+
|
|
955
|
+
Clerk 3.12 has no per-client fetch or diagnostic hook. Connecta uses a
|
|
956
|
+
generated copy of that reviewed SDK with an instance-owned fetch: JWKS,
|
|
957
|
+
backend API, opaque verification, and handshake responses read bytes through
|
|
958
|
+
`byteReadResponse`, so workerd cannot print an upstream Content-Type during a
|
|
959
|
+
native text read. SDK diagnostics use the fixed `Clerk authentication failed`
|
|
960
|
+
event and checked `logFailure` records. Its authentication and resource
|
|
961
|
+
serialization logic stays in the SDK; no global fetch, Response, or console is
|
|
962
|
+
patched. Telemetry event and log methods are inert on this client, including
|
|
963
|
+
when telemetry debug output is enabled. The optional `@clerk/backend` peer
|
|
964
|
+
accepts `^3.12.0` for the JWT decoder; it does not select the bundled SDK's
|
|
965
|
+
version. `gate` receives Connecta's `ClerkGateClient`, which declares
|
|
966
|
+
`users.getUser` and the user identity, name, and email fields used by Connecta.
|
|
967
|
+
These declarations do not import the consumer's Clerk types.
|
|
968
|
+
|
|
969
|
+
[`scripts/build-clerk-sdk.mjs`](https://github.com/zackbart/connecta/blob/main/scripts/build-clerk-sdk.mjs) verifies the
|
|
970
|
+
upstream version and source hash, applies the transport and diagnostic hooks,
|
|
971
|
+
and regenerates the adapter with its license notices. `check:clerk-sdk` checks
|
|
972
|
+
freshness in both verification loops. Updating the locked Clerk SDK requires a
|
|
973
|
+
deliberate adapter update and the real-SDK Node and Workers auth tests.
|
|
974
|
+
|
|
567
975
|
## Clerk configuration is checked at construction
|
|
568
976
|
|
|
569
|
-
`clerkAuth` reads its Frontend API origin out of `publishableKey`, so a key
|
|
570
|
-
is not `pk_test_`/`pk_live_`
|
|
571
|
-
|
|
572
|
-
the
|
|
573
|
-
|
|
574
|
-
request, as the Workers shape does, sees that same error on its first request
|
|
977
|
+
`clerkAuth` reads its Frontend API origin out of `publishableKey`, so a key
|
|
978
|
+
that is not `pk_test_`/`pk_live_` and a base64-encoded domain throws when
|
|
979
|
+
`clerkAuth` is called, as `allowedDomains` does, naming the option without
|
|
980
|
+
quoting the value — usually the _secret_ key pasted into the publishable slot.
|
|
981
|
+
A per-request deployment, as on Workers, sees that error on its first request
|
|
575
982
|
instead of a base64 stack on every route.
|
|
576
983
|
|
|
577
984
|
## Human authentication management
|
|
578
985
|
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
and
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
`
|
|
658
|
-
|
|
659
|
-
and the
|
|
660
|
-
|
|
986
|
+
Auth URL elicitation uses the SDK's `createRequestStateCodec`, backed by a
|
|
987
|
+
separate HKDF-derived HMAC-SHA256 key held inside `encryptedCredentialVault`.
|
|
988
|
+
The ten-minute state is authenticated, not encrypted. Its secret-free payload
|
|
989
|
+
binds the admitted principal, canonical endpoint including pool, connector,
|
|
990
|
+
meta-tool, address, SHA-256 digest of arguments or code, round, expiry, and
|
|
991
|
+
the opaque browser-link nonces issued in that flow. Entered reads also bind
|
|
992
|
+
their addresses and classification digests.
|
|
993
|
+
Arguments and program source never enter the payload. The absolute expiry
|
|
994
|
+
survives every round. Rotation invalidates outstanding states.
|
|
995
|
+
Each state admits one retry through a connector-scoped storage CAS, including
|
|
996
|
+
decline and cancel. Concurrent or repeated consumption is refused before
|
|
997
|
+
dispatch, so retries cannot fork into sibling forced connection links.
|
|
998
|
+
The host records each entered call's classification, catalog freshness and
|
|
999
|
+
classification digest in private invocation state, along with auth failure origins. Writes may elicit only
|
|
1000
|
+
when Connecta's own credential resolution fails before invoking the connector:
|
|
1001
|
+
a missing grant, a missing credential slot, or consent required before refresh.
|
|
1002
|
+
Auth failures inside handlers or transports cannot elicit for writes, including
|
|
1003
|
+
raw fetches, custom connector errors, and refresh failures inside a handler.
|
|
1004
|
+
Programs also stop automatic recovery once any write-classified call enters
|
|
1005
|
+
its handler, even if it sends nothing or fails. Each accepted retry checks
|
|
1006
|
+
these facts for its own round. Later auth failures
|
|
1007
|
+
carry `reconciliationRequired: true` and manual connection guidance because the
|
|
1008
|
+
call may have partially run. Post-entry recovery requires every entered call
|
|
1009
|
+
to have a read classification from a catalog accepted within its TTL. Stale
|
|
1010
|
+
fallbacks, including a failed listing followed by cached annotations, cannot
|
|
1011
|
+
authorize a re-run. Normal calls retain their existing stale-fallback behavior.
|
|
1012
|
+
Before an accepted retry restarts any call, it rechecks every entered read.
|
|
1013
|
+
A write classification, changed classification digest, or stale catalog returns
|
|
1014
|
+
`auth_replay_refused` with reconciliation guidance, including in trusted programs.
|
|
1015
|
+
Pre-invocation credential-resolution eligibility is unchanged.
|
|
1016
|
+
Custom vaults may implement the host-only `requestStateKey` method; without it, the
|
|
1017
|
+
deployment retains the ordinary handoff. It must return a stable,
|
|
1018
|
+
purpose-specific deployment key of at least 32 bytes. Only the MCP boundary
|
|
1019
|
+
reads this key; connector contexts and guest APIs cannot reach the vault.
|
|
1020
|
+
Explicit authorization retries require a completed browser start (including
|
|
1021
|
+
its reset and consent binding), valid stored credential fields where declared,
|
|
1022
|
+
and a healthy passive connector status where available before completing.
|
|
1023
|
+
An in-progress start cannot complete against an old grant. A forced flow switches to
|
|
1024
|
+
Continue after that visit, so another prompt cannot restart pending consent.
|
|
1025
|
+
Failed starts remain single-use but do not block a later successful attempt.
|
|
1026
|
+
Before checking completion, unspent links in the retry flow are atomically
|
|
1027
|
+
retired so an older forced link cannot begin a reset during the status check.
|
|
1028
|
+
A Continue link issued during a pending forced start is bound to that
|
|
1029
|
+
predecessor and cannot be consumed unless the predecessor starts successfully.
|
|
1030
|
+
If it fails, the next prompt requests a fresh restart instead of reusing the old grant.
|
|
1031
|
+
|
|
1032
|
+
The `/connect` link issued by the elicitation boundary encrypts its browser
|
|
1033
|
+
handoff with the vault's `seal`/`open` methods before signing it. The URL carries
|
|
1034
|
+
neither principal identifiers nor credentials and authenticates no browser.
|
|
1035
|
+
The same principal and management checks apply to OAuth and credential-slot
|
|
1036
|
+
links. A credential-slot visit consumes its link and redirects to the
|
|
1037
|
+
same-origin operator UI, where credential mutation requires its own identity
|
|
1038
|
+
and Origin checks. Vaults without sealing keep ordinary handoffs and cannot
|
|
1039
|
+
serve URL elicitation. See [authorization recovery](./meta-tools.md#authorization-recovery)
|
|
1040
|
+
for capability gating, retry termination, and dispatch restrictions.
|
|
1041
|
+
|
|
1042
|
+
A verified `/connect` visit gives downstream OAuth work and its state handoff 30 seconds. The request signal
|
|
1043
|
+
and deadline reach discovery, registration, and other downstream fetches; expiry returns `504 OAuth
|
|
1044
|
+
authorization start timed out`. A restart's grant write, catalog invalidation, and scope close drain before
|
|
1045
|
+
responding because storage has no cancellation contract. They can extend that wait. The provider checks
|
|
1046
|
+
cancellation before writing and removes late cancelled writes. Disconnect commits even if the browser leaves.
|
|
1047
|
+
|
|
1048
|
+
Restart carries a dynamically registered client only when its stored issuer, connector URL, redirect URI,
|
|
1049
|
+
client metadata, auth scope, transport settings, and owner partition still match. It drops the grant and flow
|
|
1050
|
+
state, selects the server again, and re-seals the client under the new epoch. A different issuer registers
|
|
1051
|
+
anew; Disconnect and issuer-mismatch recovery discard it. URL-based clients and clients with expired secrets
|
|
1052
|
+
are never carried.
|
|
1053
|
+
|
|
1054
|
+
A carried client needs a completed grant before another carry. Building a consent URL cannot detect a purged
|
|
1055
|
+
registration, and RFC 6749 forbids redirecting an unknown client to the callback. Restarting again without an
|
|
1056
|
+
intervening grant therefore registers anew. A start whose refresh returns `invalid_client` drops the client
|
|
1057
|
+
and registers in that start; a callback exchange with that error drops the client so Continue cannot return
|
|
1058
|
+
its URL.
|
|
1059
|
+
|
|
1060
|
+
Credential and OAuth mutation require interactive identity, connector visibility, management permission, and
|
|
1061
|
+
an exact same-origin `Origin` for browser mutations. A static credential `authorize_connector` call needs
|
|
1062
|
+
visibility only: with `ui: operatorUi()` and a vault it returns a secret-free `operator_config` handoff;
|
|
1063
|
+
without either it returns `unavailable`. The OAuth branch also checks management permission and returns
|
|
1064
|
+
`unavailable` when refused.
|
|
1065
|
+
|
|
1066
|
+
Core owns `/connect/<connector>` and the callback without the optional UI. Both require Clerk sessions or
|
|
1067
|
+
Cloudflare Access users; MCP OAuth tokens, `cta_` tokens, and Access service identities
|
|
1068
|
+
cannot authenticate them. Without an interactive provider, connection fails at runtime with `An interactive
|
|
1069
|
+
provider (Clerk or Cloudflare Access) is required to connect OAuth connectors.` Machine-only deployments can
|
|
1070
|
+
still serve configured credentials. Both routes accept only GET, otherwise returning 405 with `Allow: GET`;
|
|
1071
|
+
`form_post` is unsupported. Clerk's session-only policy applies regardless of method.
|
|
1072
|
+
|
|
1073
|
+
The fifteen-minute signed handoff carries connector ownership, initiating principal, deployment origin, nonce,
|
|
1074
|
+
restart mode, and any predecessor link binding. New links use versioned, length-prefixed fields instead of
|
|
1075
|
+
JSON, with unpadded base64url payload and signature segments. Ordinary handoffs use an opaque wire encoding,
|
|
1076
|
+
not encryption; URL elicitation also seals those fields as described above. Copy the complete URL unchanged.
|
|
1077
|
+
The signature authenticates the exact version and payload text. Tampered or re-encoded links return
|
|
1078
|
+
`Invalid or expired connection link. Request a new link from connecta.` Pre-upgrade signed JSON and sealed
|
|
1079
|
+
v2 links still verify against their original text and expire after their original fifteen minutes.
|
|
1080
|
+
|
|
1081
|
+
Configure `vault: encryptedCredentialVault(storage, key)`, which derives its HMAC key with HKDF. Custom vaults need `signOAuthHandoff` and `verifyOAuthHandoff`; without a signing vault, links cannot be
|
|
1082
|
+
issued or accepted. A link grants no identity. After identity and permissions pass, its nonce is consumed
|
|
1083
|
+
before OAuth starts, even while consent is pending. Failed starts need new links; nonce markers expire with
|
|
1084
|
+
their links.
|
|
1085
|
+
|
|
1086
|
+
Clerk sign-in returns to the same link. Expired session cookies use Clerk's handshake to refresh before
|
|
1087
|
+
identity checks or code exchange; responses preserve session cookies. Access supplies trusted Worker identity
|
|
1088
|
+
at the edge. Protect `/connect/*` and `/oauth/callback/*` with the MCP endpoint's Access application.
|
|
1089
|
+
|
|
1090
|
+
For both connector scopes, the browser must match the initiating principal and retain `personalConnection` or
|
|
1091
|
+
shared `credentialAdministration` permission. Connecta saves that principal against downstream state. The
|
|
1092
|
+
callback checks state, identity, permission, and RFC 9207 `iss` before consuming
|
|
1093
|
+
the handoff, interpreting an error, or exchanging a code. A supplied issuer must
|
|
1094
|
+
exactly match the consent. A server advertising issuer-response support also
|
|
1095
|
+
requires `iss`; missing or mismatched values produce the generic refusal. Custom
|
|
1096
|
+
connectors receiving `iss` need `verifyCallbackIssuer`. Verified error callbacks
|
|
1097
|
+
consume their handoff and CAS-terminate the consent once, discarding its PKCE
|
|
1098
|
+
verifier before displaying fixed copy for a known reason. Continue starts a new
|
|
1099
|
+
consent; later callbacks with the old state are refused. Custom OAuth connectors
|
|
1100
|
+
need `consumeAuthError` to accept error callbacks, sharing `finishAuth`'s atomic
|
|
1101
|
+
consent claim. Reissue
|
|
1102
|
+
pending consent links after upgrading; callbacks without a saved initiating user cannot complete.
|
|
1103
|
+
|
|
1104
|
+
`compareAndSet` atomically claims link nonces, callback handoffs, and consents, with one concurrent winner,
|
|
1105
|
+
so each completes once.
|
|
1106
|
+
[Meta-tools](./meta-tools.md#authorization-recovery) describes recovery.
|
|
1107
|
+
|
|
1108
|
+
## Downstream OAuth client registration
|
|
1109
|
+
|
|
1110
|
+
`remoteMcp(id, { auth: { type: "oauth", scope } })` defaults to a self-hosted
|
|
1111
|
+
Client ID Metadata Document when the configured `publicUrl` is a public HTTPS
|
|
1112
|
+
origin. Connecta serves `GET /oauth/client-metadata/<id>` without inbound auth,
|
|
1113
|
+
with `application/json` and a five-minute public cache lifetime. Only configured
|
|
1114
|
+
self-hosted OAuth connectors have a document; unknown, non-OAuth, external-CIMD,
|
|
1115
|
+
and static-client connectors return 404. Other methods return 405.
|
|
1116
|
+
|
|
1117
|
+
The document contains its own URL as `client_id`, `serverInfo.name` as
|
|
1118
|
+
`client_name`, the canonical `publicUrl` origin's callback in `redirect_uris`, the
|
|
1119
|
+
`authorization_code` and `refresh_token` grants, `response_types: ["code"]`,
|
|
1120
|
+
`application_type: "web"`, `token_endpoint_auth_method: "none"`, and configured
|
|
1121
|
+
`scope` when present. It contains no secrets. Alternate request hosts never add
|
|
1122
|
+
callbacks or change the client ID. The document, DCR submission, and saved-client
|
|
1123
|
+
configuration binding use one metadata builder.
|
|
1124
|
+
|
|
1125
|
+
| Configuration and authorization server | Registration path |
|
|
1126
|
+
| ------------------------------------------------------------ | --------------------------------------------- |
|
|
1127
|
+
| Pre-registered `auth.client` | `static`, restricted to its configured issuer |
|
|
1128
|
+
| Explicit `clientMetadataUrl`, server advertises CIMD support | `cimd`, using the external URL |
|
|
1129
|
+
| Public HTTPS `publicUrl`, server advertises CIMD support | `cimd`, using Connecta's document |
|
|
1130
|
+
| Server does not advertise CIMD support | `dcr` fallback |
|
|
1131
|
+
| Unset, HTTP, loopback, or private `publicUrl` | `dcr`, without a self-hosted document |
|
|
1132
|
+
|
|
1133
|
+
`startAuth()` and operator connector status expose `registrationPath` after a
|
|
1134
|
+
client has been selected. The selected mechanism is stored alongside the client,
|
|
1135
|
+
including across a restart that preserves DCR registration. Older grants without
|
|
1136
|
+
that field omit the path until a new client is selected. Reads neither register
|
|
1137
|
+
a client nor begin consent; static client information is persisted during explicit
|
|
1138
|
+
consent, not passive status.
|
|
1139
|
+
DCR fallback follows the server's advertised capabilities; a rejected CIMD
|
|
1140
|
+
consent is not silently retried under another client identity.
|
|
1141
|
+
|
|
1142
|
+
`clientMetadataUrl` remains an optional external HTTPS document on a non-root
|
|
1143
|
+
path, without credentials or a fragment. Its `client_id` must equal its URL,
|
|
1144
|
+
and its metadata must include the deployment's exact callback and public-client
|
|
1145
|
+
authentication. Changing from an external document to the self-hosted URL needs
|
|
1146
|
+
one Disconnect and reconnect because tokens belong to the old client ID.
|
|
1147
|
+
`basecamp()` now defaults to the self-hosted document and accepts an external
|
|
1148
|
+
URL override. Basecamp restricts DCR for HTTPS callbacks, so it needs a public
|
|
1149
|
+
HTTPS deployment or an external document.
|
|
1150
|
+
|
|
1151
|
+
For a pre-registered client, use:
|
|
1152
|
+
|
|
1153
|
+
```ts
|
|
1154
|
+
remoteMcp("service", {
|
|
1155
|
+
url: "https://mcp.example/mcp",
|
|
1156
|
+
auth: {
|
|
1157
|
+
type: "oauth",
|
|
1158
|
+
client: {
|
|
1159
|
+
issuer: "https://auth.example",
|
|
1160
|
+
clientId: env.SERVICE_CLIENT_ID,
|
|
1161
|
+
clientSecret: env.SERVICE_CLIENT_SECRET,
|
|
1162
|
+
tokenEndpointAuthMethod: "client_secret_basic",
|
|
1163
|
+
},
|
|
1164
|
+
},
|
|
1165
|
+
});
|
|
1166
|
+
```
|
|
1167
|
+
|
|
1168
|
+
`client` and `clientMetadataUrl` are mutually exclusive. The client must name
|
|
1169
|
+
its exact HTTPS issuer. A different discovered issuer is refused before
|
|
1170
|
+
registration or credential dispatch. Authentication defaults to
|
|
1171
|
+
`client_secret_basic` with a secret and `none` without one; `client_secret_post`
|
|
1172
|
+
is also supported. Empty credentials and mismatched methods fail construction.
|
|
1173
|
+
Client secrets stay in deployment configuration. Storage holds only the client
|
|
1174
|
+
ID and its public binding, alongside the encrypted grant. Restart discards
|
|
1175
|
+
stored static-client state and reselects the configured identity.
|
|
1176
|
+
|
|
1177
|
+
Configured `scope` is a fallback; a downstream challenge or protected-resource
|
|
1178
|
+
declaration takes precedence, and the SDK adds `offline_access` when advertised.
|
|
661
1179
|
|
|
662
1180
|
## Downstream OAuth on `api()`
|
|
663
1181
|
|
|
664
|
-
|
|
665
|
-
Church Community Builder's
|
|
666
|
-
its clients by hand, and publishes no metadata to discover. `api()` takes a
|
|
667
|
-
static authorization-code configuration for that shape:
|
|
1182
|
+
`api()` takes a static authorization-code configuration for APIs without MCP
|
|
1183
|
+
or OAuth discovery, such as Church Community Builder's manually issued clients:
|
|
668
1184
|
|
|
669
1185
|
```ts
|
|
670
1186
|
api("church", {
|
|
@@ -682,76 +1198,83 @@ api("church", {
|
|
|
682
1198
|
description: "Read one person",
|
|
683
1199
|
annotations: { readOnlyHint: true },
|
|
684
1200
|
inputSchema: { type: "object", properties: { id: { type: "string" } }, required: ["id"] },
|
|
685
|
-
handler: async ({ id }, ctx) =>
|
|
686
|
-
|
|
687
|
-
`https://api.example.com/people/${encodeURIComponent(id)}`,
|
|
688
|
-
);
|
|
689
|
-
return response.json();
|
|
690
|
-
},
|
|
1201
|
+
handler: async ({ id }, ctx) => (await ctx.oauth!.fetch(
|
|
1202
|
+
`https://api.example.com/people/${encodeURIComponent(id)}`)).json(),
|
|
691
1203
|
}],
|
|
692
1204
|
});
|
|
693
1205
|
```
|
|
694
1206
|
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
`
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
`resource` is sent. The grant is bound to the token endpoint instead:
|
|
708
|
-
repointing it fences the old tokens behind a new epoch rather than sending
|
|
709
|
-
them to the new server.
|
|
710
|
-
|
|
711
|
-
**The client lives in deployment configuration, not a vault slot.** It is the
|
|
712
|
-
deployment's identity at the provider — one per deployment, like Clerk's
|
|
713
|
-
`secretKey` — while a vault slot belongs to an owner: on a personal connector
|
|
714
|
-
every human would paste the deployment's secret into their own partition.
|
|
715
|
-
Configuration also keeps it out of storage altogether. It is never written,
|
|
716
|
-
sealed or otherwise, so a leaked store holds no client secret, Disconnect has
|
|
717
|
-
nothing of it to delete, and Restart has no registration to carry forward.
|
|
718
|
-
Read it from the environment or a Worker secret; an empty string, the usual
|
|
719
|
-
unset variable, refuses to boot without quoting it.
|
|
1207
|
+
No discovery or registration occurs; all URLs are configuration, so the
|
|
1208
|
+
[advertised-URL rule](#urls-a-downstream-advertises) does not apply.
|
|
1209
|
+
|
|
1210
|
+
| Option or binding | Contract |
|
|
1211
|
+
| ------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
1212
|
+
| Endpoints | Checked at construction: HTTPS, HTTP only on loopback, no credentials or fragment. |
|
|
1213
|
+
| `pkce` | S256 by default; `false` omits challenge and verifier. |
|
|
1214
|
+
| `authorizationParams` | Adds provider parameters; cannot restate the grant's own. |
|
|
1215
|
+
| `tokenRequestHeaders` | Adds code-exchange and every refresh's headers; cannot set `Authorization`, `Content-Type`, `Content-Length`, `Cookie`, or `Host`. |
|
|
1216
|
+
| `tokenEndpointAuthMethod` | Defaults to `client_secret_basic` with a secret, `none` without; mismatched pairings refuse construction. |
|
|
1217
|
+
| Issuer/resource | No advertised issuer means no required RFC 9207 `iss` or sent RFC 8707 `resource`. The grant binds to the token endpoint; after it changes, old tokens are never sent and the next consent replaces them. |
|
|
1218
|
+
| Client | One deployment-config identity, never written or sealed to storage; no owner vault slot. Store leaks contain no client secret; Disconnect deletes none and Restart carries none. Read from environment or Worker secrets; empty strings refuse construction without quoting values. |
|
|
720
1219
|
|
|
721
1220
|
Handlers never see the grant. `ctx.oauth.fetch(url, init)` sends the calling
|
|
722
|
-
owner's access token as `Authorization: Bearer`
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
1221
|
+
owner's access token as `Authorization: Bearer` with these rules:
|
|
1222
|
+
|
|
1223
|
+
- Only `apiOrigins` receive tokens; refuse other origins before sending, including
|
|
1224
|
+
untrusted response URLs. Return redirects unfollowed; handlers cannot set `Authorization`.
|
|
1225
|
+
- On 401, recover exactly once using an already rotated token or one coalesced
|
|
1226
|
+
refresh through `remoteMcp()`'s coordinator. Persist rotation even when its
|
|
1227
|
+
owner is cancelled after the answer. Replay once; stream bodies are refused.
|
|
1228
|
+
- No grant, a second 401, or a [dead refresh](#refresh-failures) means
|
|
1229
|
+
`downstream_oauth_required`, directing `authorize_connector`. Refresh waiter deadlines
|
|
1230
|
+
return retryable `unavailable`. Latch second 401s for the request scope,
|
|
1231
|
+
preventing repeated refreshes by later program calls.
|
|
1232
|
+
- Handlers can name no storage, sealing, or owner partition; the registry owns them.
|
|
1233
|
+
- The answer's `.text()` and `.json()`, clones' too, decode its bytes as UTF-8, so
|
|
1234
|
+
workerd never quotes a downstream's Content-Type in its own log (INV-6).
|
|
1235
|
+
|
|
1236
|
+
The grant record and consents, epoch fencing, vault sealing, shared/personal ownership,
|
|
1237
|
+
callback state/principal checks, `authorize_connector`, Connect, Restart, and
|
|
1238
|
+
Disconnect match `remoteMcp()`. Status calls report a stored grant for the
|
|
1239
|
+
configured token endpoint healthy without downstream probes and never start
|
|
1240
|
+
authorization. `oauth` and
|
|
1241
|
+
`credential` are exclusive per connector, giving `auth_required` one recovery.
|
|
1242
|
+
For providers offering both, deployment config chooses; handlers check `ctx.oauth`.
|
|
1243
|
+
|
|
1244
|
+
## Request-local downstream Bearer tokens
|
|
1245
|
+
|
|
1246
|
+
`remoteMcp({ auth: { type: "request", token: async (ctx) => … } })` resolves
|
|
1247
|
+
its token before connecting and before reusing a client. The callback receives
|
|
1248
|
+
the operation's cancellation context, never an agent-selected credential owner.
|
|
1249
|
+
Only shared ownership is accepted. Tokens remain inside the request's hardened
|
|
1250
|
+
transport, are not stored or described, and a changed token replaces the client.
|
|
1251
|
+
This mode refuses cleartext non-loopback origins. Optional `auth.headers` carry
|
|
1252
|
+
static protocol/catalog controls and cannot set Authorization.
|
|
1253
|
+
|
|
1254
|
+
The GitHub provider validates each tool's owner/repo and scope access before
|
|
1255
|
+
resolving an installation token. It creates one token-bound MCP client for that
|
|
1256
|
+
operation and closes it before returning. Completed installation/token caches
|
|
1257
|
+
contain bounded runtime values only, partitioned by key fingerprint, owner
|
|
1258
|
+
installation, repositories and permissions. No request promise or transport
|
|
1259
|
+
is retained across requests, and rejected writes are never replayed. An omitted
|
|
1260
|
+
`app.privateKey` uses the encrypted connector vault's `privateKey` field;
|
|
1261
|
+
`describe()` includes neither the key nor the minted tokens. The maintained
|
|
1262
|
+
provider skill explains App permissions and installation prerequisites.
|
|
1263
|
+
|
|
1264
|
+
## Source and tests
|
|
1265
|
+
|
|
1266
|
+
Inbound adapters live in [src/auth](https://github.com/zackbart/connecta/tree/main/src/auth);
|
|
1267
|
+
[auth routes](https://github.com/zackbart/connecta/blob/main/src/routes/oauth.ts)
|
|
1268
|
+
own connection handoffs and callbacks. Check [inbound auth tests](https://github.com/zackbart/connecta/blob/main/test/inbound-auth.test.ts),
|
|
1269
|
+
[Access tests](https://github.com/zackbart/connecta/blob/main/test/cloudflare-access-auth.test.ts),
|
|
1270
|
+
and [identity-scope tests](https://github.com/zackbart/connecta/blob/main/test/identity-scope.test.ts)
|
|
1271
|
+
for credential precedence and disclosure boundaries.
|
|
1272
|
+
|
|
1273
|
+
Downstream OAuth implementation and evidence are in
|
|
1274
|
+
[downstream-oauth.ts](https://github.com/zackbart/connecta/blob/main/src/auth/downstream-oauth.ts),
|
|
1275
|
+
[static-oauth.ts](https://github.com/zackbart/connecta/blob/main/src/auth/static-oauth.ts),
|
|
1276
|
+
[OAuth connect tests](https://github.com/zackbart/connecta/blob/main/test/oauth-connect.test.ts),
|
|
1277
|
+
[callback fencing](https://github.com/zackbart/connecta/blob/main/test/oauth-callback-fencing.test.ts),
|
|
1278
|
+
[refresh leases](https://github.com/zackbart/connecta/blob/main/test/oauth-refresh-lease.test.ts),
|
|
1279
|
+
and [API OAuth tests](https://github.com/zackbart/connecta/blob/main/test/api-oauth.test.ts).
|
|
1280
|
+
Use the test pointers within each section for narrower failure cases.
|