@zackbart/connecta 0.24.3 → 0.25.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/AGENTS.md +58 -30
- package/CHANGELOG.md +317 -1
- package/README.md +5 -6
- package/dist/advertised-schema.js +53 -0
- package/dist/auth/downstream-oauth.js +490 -106
- package/dist/branding.d.ts +31 -2
- package/dist/branding.js +116 -8
- package/dist/call-admission.js +139 -113
- package/dist/catalog-service.js +141 -105
- package/dist/catalog.js +258 -33
- package/dist/concurrency.js +24 -23
- package/dist/connector-scope.js +6 -37
- package/dist/connectors/api.d.ts +3 -3
- package/dist/connectors/api.js +10 -2
- package/dist/connectors/guarded-fetch.js +34 -4
- package/dist/connectors/remote-mcp.d.ts +6 -5
- package/dist/connectors/remote-mcp.js +325 -245
- package/dist/credential-contract.d.ts +9 -0
- package/dist/credential-rules.js +2 -2
- package/dist/credentials.d.ts +6 -1
- package/dist/credentials.js +45 -3
- package/dist/errors.d.ts +11 -3
- package/dist/errors.js +2 -1
- package/dist/execute.js +363 -257
- package/dist/executor-admission.js +67 -50
- package/dist/executors/quickjs-runtime.js +4 -2
- package/dist/executors/quickjs.js +251 -210
- package/dist/fix-prompt.js +37 -0
- package/dist/index.d.ts +11 -2
- package/dist/index.js +63 -21
- package/dist/invocation.js +324 -285
- package/dist/meta-tools.js +304 -114
- package/dist/oauth-callback-outcome.js +94 -0
- package/dist/oauth-sealing.js +29 -0
- package/dist/operator-ui/fix-prompts.js +87 -0
- package/dist/operator-ui/generated.js +2 -2
- package/dist/operator-ui/model.d.ts +46 -1
- package/dist/operator-ui/setup-commands.js +53 -0
- package/dist/operator-ui/view.js +109 -2
- package/dist/program-source.js +205 -0
- package/dist/providers/cloudflare.d.ts +14 -2
- package/dist/providers/cloudflare.js +107 -16
- package/dist/providers/linear.d.ts +26 -4
- package/dist/providers/linear.js +19 -4
- package/dist/providers/mixpanel.d.ts +16 -3
- package/dist/providers/mixpanel.js +13 -2
- package/dist/providers/notion.d.ts +8 -1
- package/dist/providers/notion.js +85 -12
- package/dist/providers/revenuecat.d.ts +30 -4
- package/dist/providers/revenuecat.js +70 -5
- package/dist/providers/stripe.d.ts +10 -3
- package/dist/providers/stripe.js +30 -4
- package/dist/providers/vercel.js +12 -2
- package/dist/registry.d.ts +13 -5
- package/dist/registry.js +317 -258
- package/dist/routes/activity.js +120 -137
- package/dist/routes/credentials.js +174 -163
- package/dist/routes/mcp.js +129 -70
- package/dist/routes/oauth-management.js +60 -71
- package/dist/routes/oauth.js +44 -16
- package/dist/routes/operator.js +82 -0
- package/dist/routes/shared.d.ts +2 -0
- package/dist/routes/ui-data.js +210 -0
- package/dist/routes/ui.js +64 -32
- package/dist/runtime/admission.js +43 -0
- package/dist/runtime/call-admission.js +67 -0
- package/dist/runtime/connector-scope.js +72 -0
- package/dist/runtime/run.js +174 -0
- package/dist/runtime/services.js +130 -0
- package/dist/runtime/storage.js +66 -0
- package/dist/server.js +111 -89
- package/dist/skills.js +1 -1
- package/dist/storage/file.js +39 -2
- package/dist/storage/memory.js +28 -16
- package/dist/timeout.js +17 -47
- package/dist/types.d.ts +64 -7
- package/dist/ui.d.ts +16 -2
- package/dist/ui.js +68 -176
- package/dist/url-safety.js +157 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/documentation/architecture.md +368 -182
- package/documentation/auth.md +277 -176
- package/documentation/code-mode.md +479 -329
- package/documentation/meta-tools.md +494 -417
- package/ethos.md +44 -47
- package/examples/worker/AGENTS.md +2 -1
- package/examples/worker/README.md +62 -14
- package/examples/worker/src/d1-storage.ts +116 -0
- package/examples/worker/src/index.ts +18 -21
- package/examples/worker/wrangler.jsonc +16 -4
- package/package.json +11 -4
- package/templates/node/.env.example +3 -3
- package/templates/node/AGENTS.md +5 -4
- package/templates/node/README.md +2 -1
- package/templates/node/package.json +1 -1
- package/templates/node/src/index.ts +23 -22
- package/dist/auth/downstream-oauth.d.ts +0 -200
- package/dist/catalog-fingerprint.d.ts +0 -19
- package/dist/catalog-limits.d.ts +0 -9
- package/dist/catalog-service.d.ts +0 -263
- package/dist/catalog.d.ts +0 -87
- package/dist/concurrency.d.ts +0 -3
- package/dist/connectors/guarded-fetch.d.ts +0 -92
- package/dist/credential-rules.d.ts +0 -85
- package/dist/execute.d.ts +0 -154
- package/dist/executor-result.d.ts +0 -10
- package/dist/executors/quickjs-child.d.ts +0 -1
- package/dist/executors/quickjs-protocol.d.ts +0 -67
- package/dist/favicon.d.ts +0 -1
- package/dist/identity.d.ts +0 -4
- package/dist/invocation.d.ts +0 -74
- package/dist/mcp-result.d.ts +0 -8
- package/dist/meta-tools.d.ts +0 -129
- package/dist/operator-ui/generated.d.ts +0 -2
- package/dist/operator-ui/view.d.ts +0 -151
- package/dist/result-shapes.d.ts +0 -13
- package/dist/retired-toolkits.d.ts +0 -2
- package/dist/routes/activity.d.ts +0 -2
- package/dist/routes/credentials.d.ts +0 -2
- package/dist/routes/mcp.d.ts +0 -10
- package/dist/routes/oauth-management.d.ts +0 -2
- package/dist/routes/oauth.d.ts +0 -2
- package/dist/routes/ui.d.ts +0 -2
- package/dist/server.d.ts +0 -9
- package/dist/skills.d.ts +0 -44
- package/dist/timeout.d.ts +0 -21
- package/dist/tool-safety.d.ts +0 -9
- package/documentation/call-admission.md +0 -158
- package/documentation/cloudflare.md +0 -471
- package/documentation/connector-guides.md +0 -176
- package/documentation/connectors.md +0 -431
- package/documentation/linear.md +0 -193
- package/documentation/mixpanel.md +0 -160
- package/documentation/notion.md +0 -308
- package/documentation/operations.md +0 -359
- package/documentation/operator-ui.md +0 -135
- package/documentation/optional-modules-upgrade.md +0 -243
- package/documentation/provider-conventions.md +0 -729
- package/documentation/request-admission.md +0 -204
- package/documentation/revenuecat.md +0 -305
- package/documentation/storage-and-credentials.md +0 -254
- package/documentation/stripe.md +0 -262
- package/documentation/upgrading.md +0 -768
- package/documentation/vercel.md +0 -241
package/documentation/auth.md
CHANGED
|
@@ -1,66 +1,87 @@
|
|
|
1
1
|
# Inbound auth
|
|
2
2
|
|
|
3
|
-
Inbound auth decides who may reach the MCP endpoint. Import
|
|
4
|
-
|
|
5
|
-
|
|
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
|
-
|
|
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
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
|
22
|
-
such as `get_result` pages
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
`
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
|
58
|
-
|
|
59
|
-
|
|
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,156 @@ createConnecta({
|
|
|
77
98
|
});
|
|
78
99
|
```
|
|
79
100
|
|
|
80
|
-
|
|
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
|
-
|
|
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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
oracle because names grant no access
|
|
93
|
-
grant I/O
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
|
+
## Downstream OAuth state at rest
|
|
153
|
+
|
|
154
|
+
With a vault configured, a `remoteMcp()` OAuth connector's tokens, dynamically
|
|
155
|
+
registered client (secret included), and PKCE verifier are sealed with the
|
|
156
|
+
vault's AES-GCM key before they reach storage. The additional authenticated data
|
|
157
|
+
names the connector, the owner partition, and the physical key, which carries
|
|
158
|
+
the authorization epoch, so ciphertext copied to another connector, principal,
|
|
159
|
+
or epoch does not open. Anything that fails to open — a tampered value, a
|
|
160
|
+
rotated key — reads as absent: the connector reports `auth_required` and logs a
|
|
161
|
+
warning. The flow bookkeeping stays plaintext: `state`, the pending URL,
|
|
162
|
+
discovery metadata, and the generation. The callback route reads `state`
|
|
163
|
+
directly, and none of it authenticates anything by itself.
|
|
164
|
+
|
|
165
|
+
Plaintext left by an older release is read, then sealed where it lies through
|
|
166
|
+
the same generation fence as any write, so an upgrade keeps the grant. Sealing
|
|
167
|
+
is one-way. An older release reads sealed state as unusable, so rolling back
|
|
168
|
+
means authorizing again. A vault without the optional `seal`/`open` members
|
|
169
|
+
keeps these values plaintext and draws a startup warning. Without any vault,
|
|
170
|
+
nothing changes: the state is plaintext, as it always was.
|
|
171
|
+
|
|
172
|
+
## Refresh failures
|
|
173
|
+
|
|
174
|
+
A failed token refresh means one of two things, and connecta decides which
|
|
175
|
+
from the token endpoint's answer, not the SDK's reading of it. A **dead grant**
|
|
176
|
+
is any 4xx except 408, 425, and 429, or a 2xx carrying an OAuth `error` (GitHub
|
|
177
|
+
answers `200 {"error":"bad_refresh_token"}`). It ends as `auth_required`, and
|
|
178
|
+
the refused tokens are deleted on the spot, so no later request or isolate
|
|
179
|
+
sends them again and the next `authorize_connector` goes straight to consent.
|
|
180
|
+
An **outage** is a 5xx, 408, 425, 429, a network failure, or a 2xx that is not
|
|
181
|
+
a token response. It ends as retryable `unavailable`, or `rate_limited` for a
|
|
182
|
+
429, with `retryAfterMs` when the server sent `Retry-After`. The grant is kept,
|
|
183
|
+
and a passive call writes no consent URL. Every request joined on the same
|
|
184
|
+
in-flight refresh gets the same verdict, even when the request that sent the
|
|
185
|
+
refresh is cancelled after the answer arrives: the refused tokens are deleted
|
|
186
|
+
before anyone waiting is released, so a request arriving meanwhile joins the
|
|
187
|
+
refusal instead of sending the dead token again.
|
|
188
|
+
|
|
189
|
+
The SDK needs this help. On a refresh failure it cannot parse, or one marked
|
|
190
|
+
`server_error`, it falls through to starting authorization, which reports a
|
|
191
|
+
healthy grant as needing consent. It rethrows every other OAuth error
|
|
192
|
+
untouched, which reports a dead grant as a generic outage and keeps re-sending
|
|
193
|
+
it. So the refresh coordinator hands the SDK an answer it classifies correctly,
|
|
194
|
+
and the provider hooks it calls next finish the job. The pinned SDK behavior is
|
|
195
|
+
spelled out beside `refreshResponseOutcome`. An explicit authorization during
|
|
196
|
+
an outage still falls through to consent, since that is what it asked for.
|
|
197
|
+
|
|
198
|
+
## URLs a downstream advertises
|
|
199
|
+
|
|
200
|
+
A `remoteMcp()` OAuth connector learns most of the URLs it fetches from the
|
|
201
|
+
downstream itself: the `resource_metadata` in its 401 challenge, the
|
|
202
|
+
`authorization_servers` in that metadata, and the token and registration
|
|
203
|
+
endpoints the authorization server publishes. Connecta fetches every one of
|
|
204
|
+
them server-side, so a compromised downstream could otherwise aim a Node or
|
|
205
|
+
Docker host at its own network — cloud metadata at `169.254.169.254`, an admin
|
|
206
|
+
panel on the LAN. Config is the security model, so the rule splits on it:
|
|
207
|
+
|
|
208
|
+
- A URL on the connector's configured origin is trusted. The operator wrote it
|
|
209
|
+
down.
|
|
210
|
+
- Any other URL must be `https` and must not name a private host: `localhost`
|
|
211
|
+
and `*.localhost`, `127/8`, `10/8`, `172.16/12`, `192.168/16`, `169.254/16`,
|
|
212
|
+
`100.64/10`, `0/8`, `::`, `::1`, `fc00::/7`, `fe80::/10`, or an IPv4-mapped
|
|
213
|
+
IPv6 form of any of those.
|
|
214
|
+
- A connector configured on a loopback host may also learn loopback URLs, over
|
|
215
|
+
`http` too, so a local MCP server with a local authorization server keeps
|
|
216
|
+
working. LAN and link-local addresses stay refused.
|
|
217
|
+
|
|
218
|
+
A refused URL is never requested. Discovery, registration, and code exchange
|
|
219
|
+
fail with a non-retryable `connector_call_failed` that names the host and
|
|
220
|
+
nothing else from the URL. A refused refresh never reaches the token endpoint,
|
|
221
|
+
so there is no verdict to act on: the grant is kept, and the SDK falls through
|
|
222
|
+
to consent as it does for any refresh it could not complete. Redirects cannot
|
|
223
|
+
route around the rule, because the redirect policy only follows same-origin
|
|
224
|
+
hops.
|
|
225
|
+
|
|
226
|
+
The check is syntactic. It reads the host after the WHATWG URL parser has
|
|
227
|
+
folded `2130706433` and `0x7f.1` into `127.0.0.1`, and it never resolves a
|
|
228
|
+
name, because the Workers-safe core has no DNS. A public name that resolves to
|
|
229
|
+
a private address, or rebinds to one after the check, is out of scope; a host
|
|
230
|
+
that must stop that runs connecta behind an egress policy that does.
|
|
231
|
+
|
|
232
|
+
## Management permissions
|
|
233
|
+
|
|
234
|
+
Visibility alone grants no authentication-management permission. Two independent
|
|
235
|
+
resolvers take `Readonly<AuthenticatedIdentity>`, return `"all"`, `"none"`, or
|
|
236
|
+
declared connector ids, and both default to `"none"`:
|
|
237
|
+
`credentialAdministration` for shared credentials and shared OAuth grants, and
|
|
238
|
+
`personalConnection` for a human's own grants on personal connectors. Each action
|
|
239
|
+
needs visibility *and* the relevant permission; both resolvers run only for an
|
|
240
|
+
interactive identity; personal actions additionally need a stable namespaced
|
|
241
|
+
principal and always use that principal's partition. Resolver exceptions and
|
|
242
|
+
unknown ids fail closed, and permissions come from authenticated identity, never
|
|
243
|
+
from caller input.
|
|
244
|
+
|
|
245
|
+
`identity.activityAccess` takes `Readonly<IdentityReference>` — `id` and
|
|
246
|
+
`namespace` — and controls reading global activity. Undeclared, it admits every
|
|
247
|
+
interactive human, the one default here that is open, because a single-operator
|
|
248
|
+
deployment would otherwise be locked out of its own event stream. Team
|
|
249
|
+
deployments should set it. There is no general administrator role and no
|
|
250
|
+
token-management authority.
|
|
132
251
|
|
|
133
252
|
```ts
|
|
134
253
|
createConnecta({
|
|
@@ -150,56 +269,47 @@ createConnecta({
|
|
|
150
269
|
});
|
|
151
270
|
```
|
|
152
271
|
|
|
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
272
|
## Cloudflare Access on Workers
|
|
159
273
|
|
|
160
274
|
[`cloudflareAccessAuth()`](https://developers.cloudflare.com/workers/configuration/cloudflare-access/)
|
|
161
|
-
is the Worker-specific path
|
|
275
|
+
is the Worker-specific path. `ethos.md` records it as **provisional**: Managed
|
|
276
|
+
OAuth and the Clerk migration still want production evidence
|
|
277
|
+
([#506](https://github.com/zackbart/connecta/issues/506)).
|
|
162
278
|
|
|
163
279
|
```ts
|
|
164
280
|
import { cloudflareAccessAuth } from
|
|
165
281
|
"@zackbart/connecta/auth/cloudflare-access";
|
|
166
282
|
|
|
167
|
-
createConnecta({
|
|
168
|
-
auth: cloudflareAccessAuth(),
|
|
169
|
-
connectors,
|
|
170
|
-
executor,
|
|
171
|
-
});
|
|
283
|
+
createConnecta({ auth: cloudflareAccessAuth(), connectors, executor });
|
|
172
284
|
```
|
|
173
285
|
|
|
174
286
|
The adapter trusts only `ctx.access`, which Cloudflare creates after Access has
|
|
175
|
-
authenticated a request that directly invokes the Worker
|
|
176
|
-
`ctx.access.getIdentity()
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
287
|
+
authenticated a request that directly invokes the Worker, and it reads identity
|
|
288
|
+
through `ctx.access.getIdentity()`. A human yields `user_uuid` or `email` as both
|
|
289
|
+
user and subject. A *service* identity yields `service_token_id`, or failing that
|
|
290
|
+
`common_name`, so distinct service tokens normally get distinct attribution;
|
|
291
|
+
either identity kind with no usable id is a 403. Only when Access returns no
|
|
292
|
+
identity at all does the adapter fall back to the Access application audience as
|
|
293
|
+
the subject — that, and only that, is the case where tokens on one application
|
|
294
|
+
share attribution. It never reads `Cf-Access-Jwt-Assertion`, downloads signing
|
|
295
|
+
keys, or accepts a JWT from the caller, and a missing context or throwing lookup
|
|
296
|
+
fails closed. It is therefore deliberately not a Node or `cloudflared` origin
|
|
297
|
+
adapter and does not survive a Service Binding hop; those shapes need their own
|
|
185
298
|
trust boundary.
|
|
186
299
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
Access decides admission and identity; Connecta configuration selects connector
|
|
191
|
-
access and these narrower permissions.
|
|
300
|
+
Access decides admission and identity; connecta configuration decides connector
|
|
301
|
+
access and management permissions. An Access service identity, having no human
|
|
302
|
+
principal, cannot mutate connection auth at all.
|
|
192
303
|
|
|
193
304
|
Protect the Worker with a Worker-level Access application whose destination is
|
|
194
305
|
`{ "type": "worker", "worker_id": "<the Worker script tag>" }`. A traditional
|
|
195
306
|
hostname-level application blocks the URL but does not attach `ctx.access` to
|
|
196
307
|
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
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
redirect URIs**:
|
|
308
|
+
on that Worker-level application for interactive MCP clients. Cloudflare then
|
|
309
|
+
owns the unauthenticated challenge and `/.well-known/` metadata, issues opaque
|
|
310
|
+
RFC 8707 tokens, and resolves them into the same trusted Worker identity.
|
|
311
|
+
Managed OAuth allows no hosted client callback by default, so enable Dynamic
|
|
312
|
+
Client Registration and add all three values to **Allowed redirect URIs**:
|
|
203
313
|
|
|
204
314
|
```text
|
|
205
315
|
https://claude.ai/api/mcp/auth_callback
|
|
@@ -208,43 +318,33 @@ https://chatgpt.com/connector/oauth/*
|
|
|
208
318
|
```
|
|
209
319
|
|
|
210
320
|
Cloudflare exposes that list as
|
|
211
|
-
`oauth_configuration.dynamic_client_registration.allowed_uris
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
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.
|
|
321
|
+
`oauth_configuration.dynamic_client_registration.allowed_uris`, on the Managed
|
|
322
|
+
OAuth settings rather than the Access policy that picks admitted identities.
|
|
323
|
+
Claude uses the first value; ChatGPT uses its stable callback or a callback-id
|
|
324
|
+
path covered by the third. For any other client add that exact URI or the
|
|
325
|
+
narrowest wildcard covering it, never the client's whole origin. Missing entries
|
|
326
|
+
let discovery succeed and registration fail later, which looks like a broken MCP
|
|
327
|
+
server rather than a console setting.
|
|
328
|
+
|
|
329
|
+
Do not add a bypass for the discovery routes; a fully automated client uses a
|
|
330
|
+
[service token](https://developers.cloudflare.com/cloudflare-one/access-controls/service-credentials/service-tokens/)
|
|
331
|
+
through `CF-Access-Client-Id` and `CF-Access-Client-Secret` instead. Worker-level
|
|
332
|
+
Access runs before every connecta route, so `/health`, operator pages, downstream
|
|
333
|
+
OAuth callbacks, and `/mcp` all require Access unless a more-specific policy says
|
|
334
|
+
otherwise, and a static connecta bearer is not a standalone edge credential
|
|
335
|
+
because Cloudflare rejects the request before connecta sees it. Custom public
|
|
336
|
+
webhooks live outside connecta and need their own Access routing policy. The
|
|
337
|
+
[Worker example](../examples/worker/) carries the whole deployment shape.
|
|
237
338
|
|
|
238
339
|
## Clerk configuration is checked at construction
|
|
239
340
|
|
|
240
341
|
`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
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
request instead of a base64 stack on every route.
|
|
342
|
+
is not `pk_test_`/`pk_live_` followed by the base64-encoded domain cannot produce
|
|
343
|
+
one. That throws where `allowedDomains` throws, when `clerkAuth` is called, naming
|
|
344
|
+
the option and never quoting the rejected value back — the usual way to land here
|
|
345
|
+
is pasting the *secret* key into the publishable slot. A deployment that builds per
|
|
346
|
+
request, as the Workers shape does, sees that same error on its first request
|
|
347
|
+
instead of a base64 stack on every route.
|
|
248
348
|
|
|
249
349
|
## Human authentication management
|
|
250
350
|
|
|
@@ -253,19 +353,20 @@ visibility, the appropriate shared or personal permission, and an exact
|
|
|
253
353
|
same-origin `Origin` for browser requests. A configured MCP bearer never becomes
|
|
254
354
|
a browser management credential.
|
|
255
355
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
`
|
|
259
|
-
|
|
260
|
-
`
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
the callback
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
356
|
+
`authorize_connector` splits along what it would change. For a connector with a
|
|
357
|
+
static credential slot it mutates nothing, so visibility is enough: with
|
|
358
|
+
`ui: operatorUi()` and a vault it returns a secret-free `operator_config` handoff
|
|
359
|
+
naming the credential fields and the operator URL, and without either the recovery
|
|
360
|
+
is `unavailable`, since connecta does not hand back a link to a missing page. Only
|
|
361
|
+
the downstream-OAuth branch consults the management permissions, and an identity
|
|
362
|
+
lacking them gets `unavailable` there.
|
|
363
|
+
|
|
364
|
+
Core owns the OAuth callback and verifies state and principal ownership
|
|
365
|
+
independently of the optional browser application. A browser returning from
|
|
366
|
+
downstream consent normally carries no MCP `Authorization` header, so an
|
|
367
|
+
interactive bearer provider's 401 does not reject the callback. The verified state
|
|
368
|
+
and its saved principal handoff select the owner; a browser identity, when
|
|
369
|
+
present, must match that owner and may then manage the connector, while an
|
|
370
|
+
interactive provider's explicit 403 still refuses the flow. See
|
|
371
|
+
[meta-tools](./meta-tools.md#authorization-recovery) for the recovery shapes a
|
|
372
|
+
caller actually receives.
|