@zackbart/connecta 0.24.3 → 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 (67) hide show
  1. package/AGENTS.md +18 -20
  2. package/CHANGELOG.md +64 -1
  3. package/README.md +5 -6
  4. package/dist/branding.d.ts +31 -2
  5. package/dist/branding.js +116 -8
  6. package/dist/connectors/api.d.ts +1 -1
  7. package/dist/connectors/api.js +10 -2
  8. package/dist/connectors/guarded-fetch.d.ts +5 -1
  9. package/dist/connectors/guarded-fetch.js +34 -4
  10. package/dist/connectors/remote-mcp.js +8 -4
  11. package/dist/errors.d.ts +11 -3
  12. package/dist/errors.js +2 -1
  13. package/dist/index.d.ts +1 -1
  14. package/dist/index.js +12 -1
  15. package/dist/meta-tools.js +105 -29
  16. package/dist/operator-ui/generated.js +2 -2
  17. package/dist/operator-ui/view.d.ts +38 -1
  18. package/dist/operator-ui/view.js +71 -0
  19. package/dist/providers/cloudflare.d.ts +14 -2
  20. package/dist/providers/cloudflare.js +107 -16
  21. package/dist/providers/linear.d.ts +26 -4
  22. package/dist/providers/linear.js +19 -4
  23. package/dist/providers/mixpanel.d.ts +16 -3
  24. package/dist/providers/mixpanel.js +13 -2
  25. package/dist/providers/notion.d.ts +8 -1
  26. package/dist/providers/notion.js +83 -10
  27. package/dist/providers/revenuecat.d.ts +30 -4
  28. package/dist/providers/revenuecat.js +42 -4
  29. package/dist/providers/stripe.d.ts +7 -1
  30. package/dist/providers/stripe.js +30 -4
  31. package/dist/providers/vercel.js +11 -1
  32. package/dist/registry.d.ts +12 -4
  33. package/dist/registry.js +22 -8
  34. package/dist/types.d.ts +37 -0
  35. package/dist/ui.js +18 -10
  36. package/dist/version.d.ts +1 -1
  37. package/dist/version.js +1 -1
  38. package/documentation/architecture.md +193 -181
  39. package/documentation/auth.md +197 -176
  40. package/documentation/code-mode.md +426 -321
  41. package/documentation/meta-tools.md +356 -416
  42. package/examples/worker/AGENTS.md +2 -1
  43. package/examples/worker/README.md +12 -10
  44. package/examples/worker/src/index.ts +12 -15
  45. package/package.json +1 -2
  46. package/templates/node/.env.example +3 -3
  47. package/templates/node/AGENTS.md +5 -4
  48. package/templates/node/README.md +2 -1
  49. package/templates/node/package.json +1 -1
  50. package/templates/node/src/index.ts +23 -22
  51. package/documentation/call-admission.md +0 -158
  52. package/documentation/cloudflare.md +0 -471
  53. package/documentation/connector-guides.md +0 -176
  54. package/documentation/connectors.md +0 -431
  55. package/documentation/linear.md +0 -193
  56. package/documentation/mixpanel.md +0 -160
  57. package/documentation/notion.md +0 -308
  58. package/documentation/operations.md +0 -359
  59. package/documentation/operator-ui.md +0 -135
  60. package/documentation/optional-modules-upgrade.md +0 -243
  61. package/documentation/provider-conventions.md +0 -729
  62. package/documentation/request-admission.md +0 -204
  63. package/documentation/revenuecat.md +0 -305
  64. package/documentation/storage-and-credentials.md +0 -254
  65. package/documentation/stripe.md +0 -262
  66. package/documentation/upgrading.md +0 -768
  67. package/documentation/vercel.md +0 -241
@@ -1,66 +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.
8
11
 
9
12
  The bearer adapter challenges with `WWW-Authenticate: Bearer` and deliberately
10
- omits `resource_metadata`. Its credential is configured out of band; it has no
11
- OAuth authorization server or registration endpoint to advertise. Interactive
12
- adapters or the edge own OAuth discovery. Every open deployment with at least
13
- one connector warns at construction, including API connectors with static auth
14
- headers. Credential and OAuth connectors add explicit wording about those grants.
15
-
16
- MCP browser origins pass the [Origin check](./request-admission.md#origin-before-admission)
17
- before admission or auth. This is independent of an identity's tool grants.
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.
18
30
 
19
31
  ## Principals, visibility, and operators
20
32
 
21
- The actor identifies the caller in activity. The subject owns transient results
22
- such as `get_result` pages. The principal is the human owner of personal
23
- connector auth. An interactive Clerk or Access user supplies all three. A
24
- Cloudflare service identity has an actor and subject but no principal.
25
-
26
- A subject or user id always selects a result-stash partition, even when the
27
- provider omits `activityActorNamespace`. An explicit namespace remains the
28
- partition namespace; without one, Connecta uses `connecta:auth:<provider kind>`.
29
- Subject ids must be distinct within that namespace. This fallback grants no
30
- personal-auth ownership and changes no activity attribution. Open deployments
31
- and providers that return no identity share one result partition. A provider
32
- that supplies only an explicit principal uses that principal as its subject.
33
-
34
- `identity.connectorAccess` returns `"all"` or a list of grants. A grant is a
35
- declared connector id, which opens every tool on it, or a `connector.tool`
36
- address, which opens that tool alone. Grants are additive, so a bare id beside
37
- addresses for the same connector means the whole connector. It governs
38
- discovery and use, and defaults to all connectors.
39
-
40
- Tool grants are enforced in the scoped registry view, below the catalog
41
- service. Since 0.24.2, `search_tools`, `describe_tools`, both call tools, a
42
- program's `connecta.search`, `connecta.describe`, and `connecta.call`, and the
43
- connection UI read that filtered tool list. Connector-level discovery, guides,
44
- and `authorize_connector` retain a connector when any tool on it is granted.
45
- In particular, a `docs.read` grant permits the `docs` authorization handoff,
46
- subject to the separate auth-management permissions below. Without any grant
47
- on `docs`, `authorize_connector` returns the same "Unknown connector" refusal
48
- as an absent connector. An ungranted tool fails exactly like one the connector
49
- never had: `unknown_tool`, with no hint that it exists. That is the whole security
50
- claim, and it lives in one place on purpose. There is no separate endpoint per
51
- tool set; an identity that should see a narrower slice is a branch in this
52
- 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.
53
71
 
54
72
  ## Pools
55
73
 
56
74
  A pool is a named slice of the deployment served at its own endpoint,
57
- `/mcp/<pool>`, for the case where one identity needs different capability
58
- sets on different clients: a support agent that sees three Notion tools and
59
- Linear, a calendar bot that sees one tool, both over the same credentials and
60
- 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.
61
78
 
62
79
  ```ts
63
80
  createConnecta({
81
+ auth: [
82
+ bearerToken(botSecret, { subjectId: "calendar-bot" }),
83
+ clerkAuth({ publishableKey, secretKey }),
84
+ ],
64
85
  pools: {
65
86
  support: {
66
87
  tools: ["linear", "notion.search_pages", "notion.fetch_page"],
@@ -77,58 +98,76 @@ createConnecta({
77
98
  });
78
99
  ```
79
100
 
80
- 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:
81
104
 
82
105
  - **A pool narrows; it never widens.** The view on `/mcp/<pool>` is the pool
83
106
  intersected with the identity's own `connectorAccess`. Plain `/mcp` is
84
- unchanged. The security boundary is still the resolver; the pool decides
85
- 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.
86
109
  - **Grant defaults to deny.** A pool with no `grant` serves nobody. Only a
87
- literal `true` admits; any other return, a throw, and an undeclared pool
88
- name produce one 404 identical in status, body, and headers, so a
89
- credential does not enumerate the other pools by response content. Timing
90
- is explicitly not hidden: a declared name awaits its grant, while an
91
- undeclared name returns without that lookup. We accept this pool-name
92
- oracle because names grant no access, a fixed delay cannot hide unbounded
93
- grant I/O, and invoking grants for unknown names would add avoidable work
94
- while holding an admission permit. Keep grants pure and fast; do not treat
95
- pool names as secrets. The operator log carries the refusal reason.
96
- - **Structural mistakes throw at construction.** A malformed name, an
97
- unknown connector, an empty pool, and a `connector.tool` address an
98
- `api()` connector's static catalog lacks all refuse to boot. Remote
99
- catalogs load lazily, so their addresses are checked at load and stay
100
- 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.
101
122
  - **OAuth discovery follows the path.** On Clerk, the 401 challenge for
102
- `/mcp/<pool>` names `/.well-known/oauth-protected-resource/mcp/<pool>`,
103
- whose `resource` is the pool URL, so RFC 9728 clients see a match.
104
- Cloudflare Managed OAuth is application-level and needs nothing.
105
-
106
- A `connector.tool` address the live catalog does not contain is unreachable
107
- and warned once while its address remains in a 1,024-entry FIFO. An evicted
108
- address may warn again; caller-derived grant text cannot grow retained warning
109
- state without bound. Remote catalogs load lazily, so construction
110
- cannot check it, and a catalog that drifts later can never widen a grant
111
- because there is no wildcard: every tool grant is an exact name.
112
-
113
- Visibility alone grants no authentication-management permission. Two
114
- independent resolvers return `"all"`, `"none"`, or declared connector ids:
115
-
116
- - `credentialAdministration` allows an interactive human to manage shared
117
- credentials and shared OAuth grants.
118
- - `personalConnection` allows an interactive human to manage their own
119
- credentials and OAuth grants on personal connectors.
120
-
121
- Both default to `"none"`. Each action requires visibility and the relevant
122
- permission. Personal actions also require a stable namespaced principal and
123
- always use that principal's partition. Resolver exceptions and unknown ids
124
- fail closed. Permissions come from authenticated identity, never caller input.
125
-
126
- The management resolvers receive `Readonly<AuthenticatedIdentity>`.
127
- `identity.activityAccess` receives `Readonly<IdentityReference>` with `id` and
128
- `namespace`, and controls reading global activity. Its default admits
129
- interactive humans, so team deployments should set it explicitly if the event
130
- stream should be restricted. It replaces `operatorAccess`; there is no general
131
- 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.
132
171
 
133
172
  ```ts
134
173
  createConnecta({
@@ -150,56 +189,47 @@ createConnecta({
150
189
  });
151
190
  ```
152
191
 
153
- Built-in Clerk and Access providers supply identity namespaces. A custom
154
- interactive provider must set `activityActorNamespace` before its users can
155
- own personal auth. Keep the namespace and principal ids stable across upgrades;
156
- changing them selects different personal storage partitions.
157
-
158
192
  ## Cloudflare Access on Workers
159
193
 
160
194
  [`cloudflareAccessAuth()`](https://developers.cloudflare.com/workers/configuration/cloudflare-access/)
161
- 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)).
162
198
 
163
199
  ```ts
164
200
  import { cloudflareAccessAuth } from
165
201
  "@zackbart/connecta/auth/cloudflare-access";
166
202
 
167
- createConnecta({
168
- auth: cloudflareAccessAuth(),
169
- connectors,
170
- executor,
171
- });
203
+ createConnecta({ auth: cloudflareAccessAuth(), connectors, executor });
172
204
  ```
173
205
 
174
206
  The adapter trusts only `ctx.access`, which Cloudflare creates after Access has
175
- authenticated a request that directly invokes the Worker. It calls
176
- `ctx.access.getIdentity()` for a human. Cloudflare returns no user identity for
177
- a service token and strips the service-token headers before invoking the
178
- Worker, so after `ctx.access` proves admission the adapter uses the Access
179
- application audience as the automation activity subject. Service tokens on the
180
- same Access application therefore share attribution. It never reads
181
- `Cf-Access-Jwt-Assertion`, downloads signing keys, or accepts a JWT from the
182
- caller. A missing context or an identity lookup that throws fails closed. This
183
- also means it is deliberately not a Node or `cloudflared` origin adapter, and
184
- 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
185
218
  trust boundary.
186
219
 
187
- A human identity gets a code-derived MCP view. Managing connection auth requires
188
- an explicit `credentialAdministration` or `personalConnection` grant. An Access
189
- service identity has no human principal and cannot mutate connection auth.
190
- Access decides admission and identity; Connecta configuration selects connector
191
- 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.
192
223
 
193
224
  Protect the Worker with a Worker-level Access application whose destination is
194
225
  `{ "type": "worker", "worker_id": "<the Worker script tag>" }`. A traditional
195
226
  hostname-level application blocks the URL but does not attach `ctx.access` to
196
227
  the Worker. Enable [**Managed OAuth**](https://developers.cloudflare.com/cloudflare-one/access-controls/applications/http-apps/managed-oauth/)
197
- on that Worker-level application for interactive MCP clients.
198
- Cloudflare then owns the unauthenticated challenge and `/.well-known/`
199
- metadata, issues opaque RFC 8707 tokens, and resolves them into the same trusted
200
- Worker identity. Managed OAuth allows no hosted client callback by default, so
201
- enable Dynamic Client Registration and add all three values to **Allowed
202
- 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**:
203
233
 
204
234
  ```text
205
235
  https://claude.ai/api/mcp/auth_callback
@@ -208,43 +238,33 @@ https://chatgpt.com/connector/oauth/*
208
238
  ```
209
239
 
210
240
  Cloudflare exposes that list as
211
- `oauth_configuration.dynamic_client_registration.allowed_uris`. It belongs to
212
- the Access application's Managed OAuth settings, not the Access policy that
213
- selects admitted identities. Claude uses the fixed first value. ChatGPT may use
214
- its stable callback or a callback-id path covered by the third value. If a
215
- client registers a different redirect, add that exact URI or the narrowest path
216
- wildcard that covers it; do not allow the client's whole origin. Without these
217
- entries discovery succeeds and client registration fails later, which makes a
218
- missing allowlist look like a broken MCP server.
219
-
220
- Do not add a bypass for the discovery routes. A fully
221
- automated client instead uses a [Cloudflare Access service token](https://developers.cloudflare.com/cloudflare-one/access-controls/service-credentials/service-tokens/)
222
- through the
223
- `CF-Access-Client-Id` and `CF-Access-Client-Secret` headers.
224
-
225
- Worker-level Access runs before every connecta route. Consequently:
226
-
227
- - `/health`, operator pages, downstream OAuth callbacks, and `/mcp` all require Access unless a more-specific hostname/path
228
- policy says otherwise;
229
- - a static Connecta bearer is not a standalone edge credential, because Cloudflare rejects them before connecta sees them; and
230
- - custom public webhooks belong to the deployment outside Connecta and need
231
- their own Access routing policy. Keep Connecta's OAuth discovery paths
232
- protected when Managed OAuth is enabled.
233
-
234
- The [Worker example](../examples/worker/) carries the complete deployment shape
235
- and the [upgrade guide](./upgrading.md#0200--0212) gives the reversible Clerk
236
- 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.
237
258
 
238
259
  ## Clerk configuration is checked at construction
239
260
 
240
261
  `clerkAuth` reads its Frontend API origin out of `publishableKey`, so a key that
241
- is not `pk_test_`/`pk_live_` followed by the base64-encoded domain cannot
242
- produce one. That throws where `allowedDomains` throws when `clerkAuth` is
243
- called with a message naming the option, never quoting the rejected value
244
- back: the usual way to land here is pasting the *secret* key into the
245
- publishable slot, and a startup error is a log line. A deployment that builds
246
- per request, as the Workers shape does, sees the same error on its first
247
- 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.
248
268
 
249
269
  ## Human authentication management
250
270
 
@@ -253,19 +273,20 @@ visibility, the appropriate shared or personal permission, and an exact
253
273
  same-origin `Origin` for browser requests. A configured MCP bearer never becomes
254
274
  a browser management credential.
255
275
 
256
- With `ui: operatorUi()` and a vault, static credential recovery can return a
257
- secret-free handoff to the connection UI. Without the UI, that recovery is
258
- `unavailable`; Connecta does not return a link to a missing page. An authorized
259
- interactive MCP caller can still start downstream OAuth through
260
- `authorize_connector` without the UI. Core owns the callback and verifies state
261
- and principal ownership independently of the optional browser application.
262
- A browser returning from downstream consent normally carries no MCP
263
- Authorization header, so an interactive bearer provider's 401 does not reject
264
- the callback. The verified state and its saved principal handoff select the
265
- owner; a browser identity, when present, must match that owner and may manage
266
- the connector. An interactive provider's explicit 403 still refuses the flow.
267
-
268
- See [meta-tools](./meta-tools.md#authorization-recovery) and
269
- [storage and credentials](./storage-and-credentials.md). The
270
- [upgrade guide](./upgrading.md#0240-optional-modules) covers moving clients
271
- 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.