@zackbart/connecta 0.24.2 → 0.24.4

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.
Files changed (97) hide show
  1. package/AGENTS.md +18 -20
  2. package/CHANGELOG.md +205 -1
  3. package/README.md +5 -6
  4. package/dist/auth/bearer.js +2 -0
  5. package/dist/auth/downstream-oauth.d.ts +12 -1
  6. package/dist/auth/downstream-oauth.js +147 -35
  7. package/dist/branding.d.ts +31 -2
  8. package/dist/branding.js +116 -8
  9. package/dist/call-admission.d.ts +4 -0
  10. package/dist/call-admission.js +26 -0
  11. package/dist/catalog-drift.js +9 -4
  12. package/dist/catalog-service.d.ts +2 -0
  13. package/dist/catalog-service.js +25 -8
  14. package/dist/catalog.d.ts +2 -0
  15. package/dist/catalog.js +246 -121
  16. package/dist/connectors/api.d.ts +1 -1
  17. package/dist/connectors/api.js +21 -3
  18. package/dist/connectors/guarded-fetch.d.ts +6 -2
  19. package/dist/connectors/guarded-fetch.js +61 -24
  20. package/dist/connectors/remote-mcp.js +92 -57
  21. package/dist/errors.d.ts +28 -3
  22. package/dist/errors.js +60 -1
  23. package/dist/execute.js +85 -23
  24. package/dist/executor-result.js +3 -1
  25. package/dist/executors/quickjs-child.js +5 -1
  26. package/dist/executors/quickjs-protocol.d.ts +4 -0
  27. package/dist/executors/quickjs-runtime.d.ts +1 -1
  28. package/dist/executors/quickjs-runtime.js +38 -21
  29. package/dist/executors/quickjs.js +68 -27
  30. package/dist/index.d.ts +15 -1
  31. package/dist/index.js +36 -4
  32. package/dist/invocation.js +134 -93
  33. package/dist/mcp-result.js +3 -2
  34. package/dist/meta-tools.js +191 -36
  35. package/dist/operator-ui/generated.js +2 -2
  36. package/dist/operator-ui/view.d.ts +38 -1
  37. package/dist/operator-ui/view.js +71 -0
  38. package/dist/providers/cloudflare.d.ts +14 -2
  39. package/dist/providers/cloudflare.js +107 -16
  40. package/dist/providers/linear.d.ts +26 -4
  41. package/dist/providers/linear.js +19 -4
  42. package/dist/providers/mixpanel.d.ts +16 -3
  43. package/dist/providers/mixpanel.js +13 -2
  44. package/dist/providers/notion.d.ts +8 -1
  45. package/dist/providers/notion.js +83 -10
  46. package/dist/providers/revenuecat.d.ts +30 -4
  47. package/dist/providers/revenuecat.js +42 -4
  48. package/dist/providers/stripe.d.ts +7 -1
  49. package/dist/providers/stripe.js +30 -4
  50. package/dist/providers/vercel.js +11 -1
  51. package/dist/registry.d.ts +22 -2
  52. package/dist/registry.js +101 -13
  53. package/dist/routes/mcp.d.ts +4 -1
  54. package/dist/routes/mcp.js +84 -13
  55. package/dist/routes/oauth.js +4 -0
  56. package/dist/routes/shared.d.ts +1 -0
  57. package/dist/routes/shared.js +4 -4
  58. package/dist/server.js +15 -3
  59. package/dist/skills.js +6 -5
  60. package/dist/storage/file.d.ts +6 -2
  61. package/dist/storage/file.js +312 -34
  62. package/dist/storage/memory.js +12 -1
  63. package/dist/types.d.ts +37 -0
  64. package/dist/ui.js +18 -10
  65. package/dist/validate.js +3 -3
  66. package/dist/version.d.ts +1 -1
  67. package/dist/version.js +1 -1
  68. package/documentation/architecture.md +193 -165
  69. package/documentation/auth.md +199 -145
  70. package/documentation/code-mode.md +433 -316
  71. package/documentation/meta-tools.md +363 -355
  72. package/examples/worker/AGENTS.md +2 -1
  73. package/examples/worker/README.md +12 -10
  74. package/examples/worker/src/index.ts +12 -15
  75. package/package.json +1 -2
  76. package/templates/node/.env.example +3 -3
  77. package/templates/node/AGENTS.md +5 -4
  78. package/templates/node/README.md +2 -1
  79. package/templates/node/package.json +1 -1
  80. package/templates/node/src/index.ts +23 -22
  81. package/documentation/call-admission.md +0 -142
  82. package/documentation/cloudflare.md +0 -471
  83. package/documentation/connector-guides.md +0 -176
  84. package/documentation/connectors.md +0 -389
  85. package/documentation/linear.md +0 -193
  86. package/documentation/mixpanel.md +0 -160
  87. package/documentation/notion.md +0 -308
  88. package/documentation/operations.md +0 -359
  89. package/documentation/operator-ui.md +0 -135
  90. package/documentation/optional-modules-upgrade.md +0 -243
  91. package/documentation/provider-conventions.md +0 -722
  92. package/documentation/request-admission.md +0 -170
  93. package/documentation/revenuecat.md +0 -305
  94. package/documentation/storage-and-credentials.md +0 -201
  95. package/documentation/stripe.md +0 -262
  96. package/documentation/upgrading.md +0 -754
  97. package/documentation/vercel.md +0 -241
@@ -1,43 +1,87 @@
1
1
  # Inbound auth
2
2
 
3
- Inbound auth decides who may reach the MCP endpoint. Import configured bearer
4
- support from `@zackbart/connecta/auth/bearer`, Clerk from `/auth/clerk`, or
5
- Cloudflare Access from `/auth/cloudflare-access`. Providers may be combined;
3
+ Inbound auth decides who may reach the MCP endpoint. Import `bearerToken` from
4
+ `@zackbart/connecta/auth/bearer`, `clerkAuth` from `/auth/clerk`, or
5
+ `cloudflareAccessAuth` from `/auth/cloudflare-access`. Providers may be combined;
6
6
  static bearers are checked first, then other providers in configuration order.
7
- Connecta no longer issues `cta_` tokens or serves token-management routes.
7
+ An `InboundAuth` provider's `authorize(request, baseUrl, runtimeContext)`
8
+ returns either `{ ok: true, userId?, subjectId?, principal? }` or a refusal
9
+ carrying its own `Response`, so the provider owns its challenge. Connecta
10
+ issues no tokens of its own and serves no token-management routes.
11
+
12
+ The bearer adapter challenges with `WWW-Authenticate: Bearer` and deliberately
13
+ omits `resource_metadata`: its credential is configured out of band, so it has no
14
+ authorization server or registration endpoint to advertise. Interactive adapters
15
+ or the edge own OAuth discovery. An open deployment with any connector warns at
16
+ construction — including API connectors carrying static auth headers, and with
17
+ sharper wording for credential and OAuth connectors.
18
+
19
+ ## Origins
20
+
21
+ `allowedOrigins?: readonly string[] | "*"` bounds which browsers may speak to
22
+ the MCP endpoint. A disallowed `Origin` on `/mcp` or any `/mcp/<pool>` gets a
23
+ fixed 403 before HTTPS redirects, admission, auth, and CORS preflight — the
24
+ check cannot depend on anything a caller has yet proved. The default admits the
25
+ `publicUrl` origin plus HTTP(S) loopback at any port; a supplied list replaces
26
+ that default rather than extending it, and `"*"` waives the check. Requests with
27
+ no `Origin` pass, since a non-browser client is not the threat here, and an
28
+ entry that is not an exact HTTP(S) origin throws at construction instead of
29
+ silently never matching. A permitted origin still has to authenticate.
8
30
 
9
31
  ## Principals, visibility, and operators
10
32
 
11
- The actor identifies the caller in activity. The subject owns transient results
12
- such as `get_result` pages. The principal is the human owner of personal
13
- connector auth. An interactive Clerk or Access user supplies all three. A
14
- Cloudflare service identity has an actor and subject but no principal.
15
-
16
- `identity.connectorAccess` returns `"all"` or a list of grants. A grant is a
17
- declared connector id, which opens every tool on it, or a `connector.tool`
18
- address, which opens that tool alone. Grants are additive, so a bare id beside
19
- addresses for the same connector means the whole connector. It governs
20
- discovery and use, and defaults to all connectors.
21
-
22
- Tool grants are enforced in the scoped registry view, below the catalog
23
- service, so `search_tools`, `describe_tools`, both call tools, a program's
24
- `connecta.search` and `connecta.call`, and the connection UI all read the same
25
- filtered list. An ungranted tool fails exactly like one the connector never
26
- had: `unknown_tool`, with no hint that it exists. That is the whole security
27
- claim, and it lives in one place on purpose. There is no separate endpoint per
28
- tool set; an identity that should see a narrower slice is a branch in this
29
- resolver, and a bot that needs its own slice is its own bearer subject.
33
+ One authorization yields three roles. The **actor** identifies the caller in
34
+ activity, the **subject** owns transient results such as `get_result` pages, and
35
+ the **principal** is the human owner of personal connector auth. An interactive
36
+ Clerk or Access user supplies all three; a Cloudflare service identity has an
37
+ actor and subject but no principal.
38
+
39
+ The principal is whichever comes first: an explicit `principal: { namespace, id }`
40
+ returned by `authorize`, accepted whenever it validates, or else one derived from
41
+ `userId` plus the provider's `activityActorNamespace`. A provider that returns its
42
+ own principal therefore needs no namespace declared; one relying on the derived
43
+ path does.
44
+
45
+ A subject or user id always selects a result-stash partition, even with no
46
+ `activityActorNamespace` declared the namespace is then
47
+ `connecta:auth:<provider kind>`, and subject ids must be distinct within it. That
48
+ fallback grants no personal-auth ownership and changes no activity attribution. A
49
+ provider supplying only an explicit principal uses it as the subject too. Open
50
+ deployments and providers that return no identity share one result partition.
51
+
52
+ `identity.connectorAccess` returns `"all"` — the default — or a list of grants:
53
+ a declared connector id opens every tool on it, a `connector.tool` address opens
54
+ that tool alone, and grants are additive, so a bare id beside addresses for the
55
+ same connector means the whole connector. It governs discovery and use alike.
56
+
57
+ Tool grants are enforced in the scoped registry view, below the catalog service,
58
+ so `search_tools`, both call tools, a program's `connecta.search`,
59
+ `connecta.describe`, and `connecta.call`, and the connection UI all read the same
60
+ filtered list. Connector-level discovery, guides, and `authorize_connector` keep
61
+ a connector when any tool on it is granted, so a `docs.read` grant permits the
62
+ `docs` authorization handoff. Without any grant on `docs`, `authorize_connector`
63
+ returns the same "Unknown connector" refusal as an absent connector, and an
64
+ ungranted tool fails exactly like one the connector never had: `unknown_tool`,
65
+ with no hint that it exists. That is the whole security claim, and it lives in
66
+ one place on purpose.
67
+
68
+ There is no *caller-selected* tool set. A narrower slice is a branch in this
69
+ resolver or a config-declared pool; a bot that needs its own slice is its own
70
+ bearer subject. What a request may never do is name its own scope.
30
71
 
31
72
  ## Pools
32
73
 
33
74
  A pool is a named slice of the deployment served at its own endpoint,
34
- `/mcp/<pool>`, for the case where one identity needs different capability
35
- sets on different clients: a support agent that sees three Notion tools and
36
- Linear, a calendar bot that sees one tool, both over the same credentials and
37
- catalog cache.
75
+ `/mcp/<pool>`, for when one identity needs different capability sets on different
76
+ clients: a support agent that sees three Notion tools and Linear, a calendar bot
77
+ that sees one tool, both over the same credentials and catalog cache.
38
78
 
39
79
  ```ts
40
80
  createConnecta({
81
+ auth: [
82
+ bearerToken(botSecret, { subjectId: "calendar-bot" }),
83
+ clerkAuth({ publishableKey, secretKey }),
84
+ ],
41
85
  pools: {
42
86
  support: {
43
87
  tools: ["linear", "notion.search_pages", "notion.fetch_page"],
@@ -54,53 +98,76 @@ createConnecta({
54
98
  });
55
99
  ```
56
100
 
57
- The rules, each of which is a test:
101
+ Pools are meaningless without configured `auth`: an open deployment builds one
102
+ anonymous, non-interactive identity, so grants like these evaluate false and
103
+ every pool path 404s. The rules, each of which is a test:
58
104
 
59
105
  - **A pool narrows; it never widens.** The view on `/mcp/<pool>` is the pool
60
106
  intersected with the identity's own `connectorAccess`. Plain `/mcp` is
61
- unchanged. The security boundary is still the resolver; the pool decides
62
- which part of it a given client sees.
107
+ unchanged. The security boundary is still the resolver; the pool decides which
108
+ part of it a given client sees.
63
109
  - **Grant defaults to deny.** A pool with no `grant` serves nobody. Only a
64
- literal `true` admits; any other return, a throw, and an undeclared pool
65
- name produce one 404 identical in status, body, and headers, so a
66
- credential does not enumerate the other pools by response. Keep grants
67
- pure and fast: a grant that does I/O is the one thing that could make a
68
- declared pool distinguishable from an undeclared one by timing. The
69
- operator log carries the reason.
70
- - **Structural mistakes throw at construction.** A malformed name, an
71
- unknown connector, an empty pool, and a `connector.tool` address an
72
- `api()` connector's static catalog lacks all refuse to boot. Remote
73
- catalogs load lazily, so their addresses are checked at load and stay
74
- unreachable until they match.
110
+ literal `true` admits; any other return, a throw, and an undeclared pool name
111
+ produce one 404 identical in status, body, and headers, reached only after auth
112
+ succeeds so pool names are not anonymously enumerable, and a valid credential
113
+ cannot tell the three cases apart by response content. Timing is not hidden: a
114
+ declared name awaits its grant while an undeclared name returns without that
115
+ lookup. We accept that oracle because names grant no access and a fixed delay
116
+ could not hide unbounded grant I/O anyway. Keep grants pure and fast; don't
117
+ treat pool names as secrets. The operator log carries the refusal reason.
118
+ - **Structural mistakes throw at construction.** A malformed name, an unknown
119
+ option, an unknown connector, an empty pool, and a `connector.tool` address an
120
+ `api()` connector's static catalog lacks all refuse to boot. Remote catalogs
121
+ load lazily, so their addresses are checked at load instead.
75
122
  - **OAuth discovery follows the path.** On Clerk, the 401 challenge for
76
- `/mcp/<pool>` names `/.well-known/oauth-protected-resource/mcp/<pool>`,
77
- whose `resource` is the pool URL, so RFC 9728 clients see a match.
78
- Cloudflare Managed OAuth is application-level and needs nothing.
79
-
80
- A `connector.tool` address the live catalog does not contain is unreachable
81
- and warned once per isolate. Remote catalogs load lazily, so construction
82
- cannot check it, and a catalog that drifts later can never widen a grant
83
- because there is no wildcard: every tool grant is an exact name.
84
-
85
- Visibility alone grants no authentication-management permission. Two
86
- independent resolvers return `"all"`, `"none"`, or declared connector ids:
87
-
88
- - `credentialAdministration` allows an interactive human to manage shared
89
- credentials and shared OAuth grants.
90
- - `personalConnection` allows an interactive human to manage their own
91
- credentials and OAuth grants on personal connectors.
92
-
93
- Both default to `"none"`. Each action requires visibility and the relevant
94
- permission. Personal actions also require a stable namespaced principal and
95
- always use that principal's partition. Resolver exceptions and unknown ids
96
- fail closed. Permissions come from authenticated identity, never caller input.
97
-
98
- The management resolvers receive `Readonly<AuthenticatedIdentity>`.
99
- `identity.activityAccess` receives `Readonly<IdentityReference>` with `id` and
100
- `namespace`, and controls reading global activity. Its default admits
101
- interactive humans, so team deployments should set it explicitly if the event
102
- stream should be restricted. It replaces `operatorAccess`; there is no general
103
- administrator role or token-management authority.
123
+ `/mcp/<pool>` names `/.well-known/oauth-protected-resource/mcp/<pool>`, whose
124
+ `resource` is the pool URL, so RFC 9728 clients see a match. Cloudflare
125
+ Managed OAuth is application-level and needs nothing.
126
+
127
+ An address the live catalog does not contain is unreachable and warned once
128
+ while it sits in a 1,024-entry FIFO, so caller-derived grant text cannot grow
129
+ retained warning state without bound. Later catalog drift can never widen a
130
+ grant: there is no wildcard, and every tool grant is an exact name.
131
+
132
+ ## Shared and personal auth
133
+
134
+ Connector auth defaults to `authScope: "shared"` — its credential, OAuth state,
135
+ tokens, catalog cache, and connector storage belong to the deployment. Set
136
+ `authScope: "personal"` when every human needs a separate downstream account:
137
+
138
+ ```ts
139
+ remoteMcp("linear", { url: "https://mcp.linear.app/mcp", authScope: "personal",
140
+ auth: { type: "oauth" } });
141
+ ```
142
+
143
+ A personal connector is absent — not refused — from any request without a
144
+ stable namespaced principal. For a principal that can see one, connecta
145
+ partitions connector storage, vault records, catalog caches, OAuth generations,
146
+ and observed result shapes under an opaque SHA-256 identity key. Keep namespaces
147
+ and principal ids stable across upgrades; changing either selects different
148
+ partitions. Literal `auth: { type: "headers" }` cannot be personal, because its
149
+ secret lives in deployment code; `remoteMcp()` refuses that combination at
150
+ construction.
151
+
152
+ ## Management permissions
153
+
154
+ Visibility alone grants no authentication-management permission. Two independent
155
+ resolvers take `Readonly<AuthenticatedIdentity>`, return `"all"`, `"none"`, or
156
+ declared connector ids, and both default to `"none"`:
157
+ `credentialAdministration` for shared credentials and shared OAuth grants, and
158
+ `personalConnection` for a human's own grants on personal connectors. Each action
159
+ needs visibility *and* the relevant permission; both resolvers run only for an
160
+ interactive identity; personal actions additionally need a stable namespaced
161
+ principal and always use that principal's partition. Resolver exceptions and
162
+ unknown ids fail closed, and permissions come from authenticated identity, never
163
+ from caller input.
164
+
165
+ `identity.activityAccess` takes `Readonly<IdentityReference>` — `id` and
166
+ `namespace` — and controls reading global activity. Undeclared, it admits every
167
+ interactive human, the one default here that is open, because a single-operator
168
+ deployment would otherwise be locked out of its own event stream. Team
169
+ deployments should set it. There is no general administrator role and no
170
+ token-management authority.
104
171
 
105
172
  ```ts
106
173
  createConnecta({
@@ -122,56 +189,47 @@ createConnecta({
122
189
  });
123
190
  ```
124
191
 
125
- Built-in Clerk and Access providers supply identity namespaces. A custom
126
- interactive provider must set `activityActorNamespace` before its users can
127
- own personal auth. Keep the namespace and principal ids stable across upgrades;
128
- changing them selects different personal storage partitions.
129
-
130
192
  ## Cloudflare Access on Workers
131
193
 
132
194
  [`cloudflareAccessAuth()`](https://developers.cloudflare.com/workers/configuration/cloudflare-access/)
133
- is the Worker-specific path:
195
+ is the Worker-specific path. `ethos.md` records it as **provisional**: Managed
196
+ OAuth and the Clerk migration still want production evidence
197
+ ([#506](https://github.com/zackbart/connecta/issues/506)).
134
198
 
135
199
  ```ts
136
200
  import { cloudflareAccessAuth } from
137
201
  "@zackbart/connecta/auth/cloudflare-access";
138
202
 
139
- createConnecta({
140
- auth: cloudflareAccessAuth(),
141
- connectors,
142
- executor,
143
- });
203
+ createConnecta({ auth: cloudflareAccessAuth(), connectors, executor });
144
204
  ```
145
205
 
146
206
  The adapter trusts only `ctx.access`, which Cloudflare creates after Access has
147
- authenticated a request that directly invokes the Worker. It calls
148
- `ctx.access.getIdentity()` for a human. Cloudflare returns no user identity for
149
- a service token and strips the service-token headers before invoking the
150
- Worker, so after `ctx.access` proves admission the adapter uses the Access
151
- application audience as the automation activity subject. Service tokens on the
152
- same Access application therefore share attribution. It never reads
153
- `Cf-Access-Jwt-Assertion`, downloads signing keys, or accepts a JWT from the
154
- caller. A missing context or an identity lookup that throws fails closed. This
155
- also means it is deliberately not a Node or `cloudflared` origin adapter, and
156
- it does not survive a Service Binding hop: those shapes need their own explicit
207
+ authenticated a request that directly invokes the Worker, and it reads identity
208
+ through `ctx.access.getIdentity()`. A human yields `user_uuid` or `email` as both
209
+ user and subject. A *service* identity yields `service_token_id`, or failing that
210
+ `common_name`, so distinct service tokens normally get distinct attribution;
211
+ either identity kind with no usable id is a 403. Only when Access returns no
212
+ identity at all does the adapter fall back to the Access application audience as
213
+ the subject that, and only that, is the case where tokens on one application
214
+ share attribution. It never reads `Cf-Access-Jwt-Assertion`, downloads signing
215
+ keys, or accepts a JWT from the caller, and a missing context or throwing lookup
216
+ fails closed. It is therefore deliberately not a Node or `cloudflared` origin
217
+ adapter and does not survive a Service Binding hop; those shapes need their own
157
218
  trust boundary.
158
219
 
159
- A human identity gets a code-derived MCP view. Managing connection auth requires
160
- an explicit `credentialAdministration` or `personalConnection` grant. An Access
161
- service identity has no human principal and cannot mutate connection auth.
162
- Access decides admission and identity; Connecta configuration selects connector
163
- access and these narrower permissions.
220
+ Access decides admission and identity; connecta configuration decides connector
221
+ access and management permissions. An Access service identity, having no human
222
+ principal, cannot mutate connection auth at all.
164
223
 
165
224
  Protect the Worker with a Worker-level Access application whose destination is
166
225
  `{ "type": "worker", "worker_id": "<the Worker script tag>" }`. A traditional
167
226
  hostname-level application blocks the URL but does not attach `ctx.access` to
168
227
  the Worker. Enable [**Managed OAuth**](https://developers.cloudflare.com/cloudflare-one/access-controls/applications/http-apps/managed-oauth/)
169
- on that Worker-level application for interactive MCP clients.
170
- Cloudflare then owns the unauthenticated challenge and `/.well-known/`
171
- metadata, issues opaque RFC 8707 tokens, and resolves them into the same trusted
172
- Worker identity. Managed OAuth allows no hosted client callback by default, so
173
- enable Dynamic Client Registration and add all three values to **Allowed
174
- redirect URIs**:
228
+ on that Worker-level application for interactive MCP clients. Cloudflare then
229
+ owns the unauthenticated challenge and `/.well-known/` metadata, issues opaque
230
+ RFC 8707 tokens, and resolves them into the same trusted Worker identity.
231
+ Managed OAuth allows no hosted client callback by default, so enable Dynamic
232
+ Client Registration and add all three values to **Allowed redirect URIs**:
175
233
 
176
234
  ```text
177
235
  https://claude.ai/api/mcp/auth_callback
@@ -180,43 +238,33 @@ https://chatgpt.com/connector/oauth/*
180
238
  ```
181
239
 
182
240
  Cloudflare exposes that list as
183
- `oauth_configuration.dynamic_client_registration.allowed_uris`. It belongs to
184
- the Access application's Managed OAuth settings, not the Access policy that
185
- selects admitted identities. Claude uses the fixed first value. ChatGPT may use
186
- its stable callback or a callback-id path covered by the third value. If a
187
- client registers a different redirect, add that exact URI or the narrowest path
188
- wildcard that covers it; do not allow the client's whole origin. Without these
189
- entries discovery succeeds and client registration fails later, which makes a
190
- missing allowlist look like a broken MCP server.
191
-
192
- Do not add a bypass for the discovery routes. A fully
193
- automated client instead uses a [Cloudflare Access service token](https://developers.cloudflare.com/cloudflare-one/access-controls/service-credentials/service-tokens/)
194
- through the
195
- `CF-Access-Client-Id` and `CF-Access-Client-Secret` headers.
196
-
197
- Worker-level Access runs before every connecta route. Consequently:
198
-
199
- - `/health`, operator pages, downstream OAuth callbacks, and `/mcp` all require Access unless a more-specific hostname/path
200
- policy says otherwise;
201
- - a static Connecta bearer is not a standalone edge credential, because Cloudflare rejects them before connecta sees them; and
202
- - custom public webhooks belong to the deployment outside Connecta and need
203
- their own Access routing policy. Keep Connecta's OAuth discovery paths
204
- protected when Managed OAuth is enabled.
205
-
206
- The [Worker example](../examples/worker/) carries the complete deployment shape
207
- and the [upgrade guide](./upgrading.md#0200--0212) gives the reversible Clerk
208
- migration.
241
+ `oauth_configuration.dynamic_client_registration.allowed_uris`, on the Managed
242
+ OAuth settings rather than the Access policy that picks admitted identities.
243
+ Claude uses the first value; ChatGPT uses its stable callback or a callback-id
244
+ path covered by the third. For any other client add that exact URI or the
245
+ narrowest wildcard covering it, never the client's whole origin. Missing entries
246
+ let discovery succeed and registration fail later, which looks like a broken MCP
247
+ server rather than a console setting.
248
+
249
+ Do not add a bypass for the discovery routes; a fully automated client uses a
250
+ [service token](https://developers.cloudflare.com/cloudflare-one/access-controls/service-credentials/service-tokens/)
251
+ through `CF-Access-Client-Id` and `CF-Access-Client-Secret` instead. Worker-level
252
+ Access runs before every connecta route, so `/health`, operator pages, downstream
253
+ OAuth callbacks, and `/mcp` all require Access unless a more-specific policy says
254
+ otherwise, and a static connecta bearer is not a standalone edge credential
255
+ because Cloudflare rejects the request before connecta sees it. Custom public
256
+ webhooks live outside connecta and need their own Access routing policy. The
257
+ [Worker example](../examples/worker/) carries the whole deployment shape.
209
258
 
210
259
  ## Clerk configuration is checked at construction
211
260
 
212
261
  `clerkAuth` reads its Frontend API origin out of `publishableKey`, so a key that
213
- is not `pk_test_`/`pk_live_` followed by the base64-encoded domain cannot
214
- produce one. That throws where `allowedDomains` throws when `clerkAuth` is
215
- called with a message naming the option, never quoting the rejected value
216
- back: the usual way to land here is pasting the *secret* key into the
217
- publishable slot, and a startup error is a log line. A deployment that builds
218
- per request, as the Workers shape does, sees the same error on its first
219
- request instead of a base64 stack on every route.
262
+ is not `pk_test_`/`pk_live_` followed by the base64-encoded domain cannot produce
263
+ one. That throws where `allowedDomains` throws, when `clerkAuth` is called, naming
264
+ the option and never quoting the rejected value back the usual way to land here
265
+ is pasting the *secret* key into the publishable slot. A deployment that builds per
266
+ request, as the Workers shape does, sees that same error on its first request
267
+ instead of a base64 stack on every route.
220
268
 
221
269
  ## Human authentication management
222
270
 
@@ -225,14 +273,20 @@ visibility, the appropriate shared or personal permission, and an exact
225
273
  same-origin `Origin` for browser requests. A configured MCP bearer never becomes
226
274
  a browser management credential.
227
275
 
228
- With `ui: operatorUi()` and a vault, static credential recovery can return a
229
- secret-free handoff to the connection UI. Without the UI, that recovery is
230
- `unavailable`; Connecta does not return a link to a missing page. An authorized
231
- interactive MCP caller can still start downstream OAuth through
232
- `authorize_connector` without the UI. Core owns the callback and verifies state
233
- and principal ownership independently of the optional browser application.
234
-
235
- See [meta-tools](./meta-tools.md#authorization-recovery) and
236
- [storage and credentials](./storage-and-credentials.md). The
237
- [upgrade guide](./upgrading.md#0240-optional-modules) covers moving clients
238
- off removed Connecta-issued tokens before changing deployment configuration.
276
+ `authorize_connector` splits along what it would change. For a connector with a
277
+ static credential slot it mutates nothing, so visibility is enough: with
278
+ `ui: operatorUi()` and a vault it returns a secret-free `operator_config` handoff
279
+ naming the credential fields and the operator URL, and without either the recovery
280
+ is `unavailable`, since connecta does not hand back a link to a missing page. Only
281
+ the downstream-OAuth branch consults the management permissions, and an identity
282
+ lacking them gets `unavailable` there.
283
+
284
+ Core owns the OAuth callback and verifies state and principal ownership
285
+ independently of the optional browser application. A browser returning from
286
+ downstream consent normally carries no MCP `Authorization` header, so an
287
+ interactive bearer provider's 401 does not reject the callback. The verified state
288
+ and its saved principal handoff select the owner; a browser identity, when
289
+ present, must match that owner and may then manage the connector, while an
290
+ interactive provider's explicit 403 still refuses the flow. See
291
+ [meta-tools](./meta-tools.md#authorization-recovery) for the recovery shapes a
292
+ caller actually receives.