@zackbart/connecta 0.25.0 → 0.26.1
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 +6 -5
- package/CHANGELOG.md +387 -0
- package/README.md +32 -15
- package/SECURITY.md +6 -5
- package/bin/connecta.mjs +5 -1
- package/dist/activity.d.ts +10 -3
- package/dist/activity.js +10 -1
- package/dist/artifacts/connector.d.ts +55 -0
- package/dist/artifacts/connector.js +724 -0
- package/dist/artifacts/document.js +80 -0
- package/dist/artifacts/guide.js +140 -0
- package/dist/artifacts/html-scan.js +329 -0
- package/dist/artifacts/json.js +68 -0
- package/dist/artifacts/kv-store.d.ts +20 -0
- package/dist/artifacts/kv-store.js +140 -0
- package/dist/artifacts/markdown.js +478 -0
- package/dist/artifacts/module.d.ts +37 -0
- package/dist/artifacts/module.js +60 -0
- package/dist/artifacts/operations.d.ts +195 -0
- package/dist/artifacts/operations.js +944 -0
- package/dist/artifacts/refresh.d.ts +53 -0
- package/dist/artifacts/refresh.js +235 -0
- package/dist/artifacts/routes.js +217 -0
- package/dist/artifacts/types.d.ts +214 -0
- package/dist/artifacts/types.js +22 -0
- package/dist/artifacts/validate.d.ts +62 -0
- package/dist/artifacts/validate.js +653 -0
- package/dist/artifacts.d.ts +6 -0
- package/dist/artifacts.js +9 -0
- package/dist/auth/downstream-oauth.js +397 -48
- package/dist/auth/oauth-start-reset.js +20 -0
- package/dist/branding.d.ts +87 -0
- package/dist/branding.js +170 -1
- package/dist/catalog-service.js +39 -29
- package/dist/catalog.js +42 -8
- package/dist/connector-access.d.ts +18 -4
- package/dist/connector-access.js +65 -20
- package/dist/connector-caller.d.ts +8 -0
- package/dist/connector-caller.js +19 -0
- package/dist/connectors/remote-mcp.js +48 -9
- package/dist/errors.d.ts +16 -1
- package/dist/errors.js +28 -0
- package/dist/execute.js +431 -75
- package/dist/executor-admission.d.ts +11 -1
- package/dist/executor-admission.js +79 -14
- package/dist/executors/quickjs.js +4 -2
- package/dist/index.d.ts +62 -6
- package/dist/index.js +230 -6
- package/dist/invocation.js +56 -10
- package/dist/meta-tools.js +7 -2
- package/dist/module-contracts.d.ts +21 -1
- package/dist/oauth-callback-outcome.js +43 -18
- package/dist/operator-ui/generated.js +2 -2
- package/dist/operator-ui/model.d.ts +62 -5
- package/dist/operator-ui/view.js +372 -28
- package/dist/page-styles.js +7 -0
- package/dist/program-source.js +116 -0
- package/dist/registry.d.ts +42 -6
- package/dist/registry.js +164 -60
- package/dist/resumable.js +1291 -0
- package/dist/routes/activity.js +6 -69
- package/dist/routes/actor-labels.js +114 -0
- package/dist/routes/credentials.js +27 -5
- package/dist/routes/mcp.js +120 -59
- package/dist/routes/oauth-management.js +87 -20
- package/dist/routes/oauth.js +54 -87
- package/dist/routes/operator.js +1 -0
- package/dist/routes/shared.d.ts +23 -1
- package/dist/routes/shared.js +20 -2
- package/dist/routes/ui-data.js +2 -1
- package/dist/routes/ui.js +19 -4
- package/dist/run-journal.js +347 -0
- package/dist/runtime/shared-read.js +106 -0
- package/dist/server.js +30 -2
- package/dist/skills.js +30 -11
- package/dist/timeout.js +26 -0
- package/dist/tool-safety.d.ts +35 -0
- package/dist/tool-safety.js +31 -0
- package/dist/types.d.ts +23 -0
- package/dist/ui.d.ts +17 -5
- package/dist/ui.js +51 -68
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/documentation/architecture.md +214 -27
- package/documentation/auth.md +186 -2
- package/documentation/code-mode.md +236 -63
- package/documentation/meta-tools.md +55 -29
- package/ethos.md +12 -10
- package/examples/worker/AGENTS.md +6 -0
- package/examples/worker/README.md +174 -10
- package/examples/worker/src/d1-activity-row.ts +14 -2
- package/examples/worker/src/d1-activity.ts +4 -2
- package/examples/worker/src/index.ts +36 -1
- package/examples/worker/src/r2-artifact-blobs.ts +29 -0
- package/examples/worker/wrangler.jsonc +5 -1
- package/package.json +6 -2
- package/templates/node/AGENTS.md +9 -2
- package/templates/node/README.md +31 -3
- package/templates/node/package.json +1 -1
- package/templates/node/src/index.ts +27 -0
|
@@ -61,7 +61,7 @@ top to bottom.
|
|
|
61
61
|
| 2 | MCP preflight | Allowed `OPTIONS` on `/mcp*`: 204 without admission or auth. |
|
|
62
62
|
| 2 | Other `OPTIONS` | Auth metadata first, otherwise compatibility CORS preflight. |
|
|
63
63
|
| 3 | `/.well-known/*` | Auth metadata, or 404. |
|
|
64
|
-
| 4 | `/health` | Open and payload-free: health, executor, admission, and deployment metadata, with drift as stable short hashes. |
|
|
64
|
+
| 4 | `/health` | Open and payload-free: health, executor, whether resumable writes are on, admission, and deployment metadata, with drift as stable short hashes. |
|
|
65
65
|
| 5 | `/oauth/callback/<connectorId>` | Core downstream OAuth completion, state and personal-ownership checked, independent of the UI. |
|
|
66
66
|
| 6 | `/mcp`, `/mcp/<pool>` | Admission, then auth, then a request-local MCP server. An undeclared pool, a refusing grant, and a throwing grant are one identical 404; see [pools](./auth.md#pools). |
|
|
67
67
|
| 7 | Other paths | 404. Custom HTTP routes belong to the deployment. |
|
|
@@ -77,8 +77,8 @@ An admitted non-preflight `/mcp` request then takes five steps in
|
|
|
77
77
|
|
|
78
78
|
1. **Admit.** One permit from the deployment-wide pool, taken before auth so an
|
|
79
79
|
unauthenticated flood costs a permit rather than a Clerk lookup, and held
|
|
80
|
-
until the response *body* completes
|
|
81
|
-
handler returns.
|
|
80
|
+
until the response *body* completes, the caller leaves, or its configured
|
|
81
|
+
lifetime ends, not until the handler returns.
|
|
82
82
|
2. **Authorize.** Each `InboundAuth` provider's `authorize` in order, bearer
|
|
83
83
|
before interactive. First `ok` admits; if all fail, the last provider's
|
|
84
84
|
challenge is returned. No providers means open — development only, and it
|
|
@@ -94,12 +94,12 @@ An admitted non-preflight `/mcp` request then takes five steps in
|
|
|
94
94
|
5. **Serve.** Refuse `?toolkit=` with a 404 — the toolkits are gone
|
|
95
95
|
([#178](https://github.com/zackbart/connecta/issues/178)) but their URLs were
|
|
96
96
|
handed out, and retiring a scoping boundary into fail-open is worse than any
|
|
97
|
-
404 — then register the
|
|
97
|
+
404 — then register the eight meta-tools on a fresh `McpServer`
|
|
98
98
|
(`test/server.test.ts`, `test/code-first-surface.test.ts`).
|
|
99
99
|
|
|
100
100
|
## Layers below the meta-tools
|
|
101
101
|
|
|
102
|
-
The meta-tool handlers are thin. The work sits in
|
|
102
|
+
The meta-tool handlers are thin. The work sits in six modules the registry owns
|
|
103
103
|
or hands out, and a change usually belongs in exactly one of them:
|
|
104
104
|
|
|
105
105
|
| Module | Owns |
|
|
@@ -109,6 +109,7 @@ or hands out, and a change usually belongs in exactly one of them:
|
|
|
109
109
|
| `src/invocation.ts` | One tool call: argument validation, call admission, one-attempt timeout, provider retry hints, result unwrapping, size capping, and the activity record. |
|
|
110
110
|
| `src/catalog.ts` | Ranking, description summarizing, and the compact and TypeScript schema renderers discovery shows. |
|
|
111
111
|
| `src/result-shapes.ts` | Bounded runtime-only inference and merging for output shapes learned from successful read-only calls whose providers declared none. |
|
|
112
|
+
| `src/resumable.ts`, `src/run-journal.ts` | Resumable writes: a program's host-call numbering, the write gate that pauses it, the journal a paused run becomes, the replay `resume_execution` plays from it, and the claim and write-ahead marks that keep an approved write to at most one send. |
|
|
112
113
|
|
|
113
114
|
`src/meta-tools.ts` and `src/execute.ts` are two front doors onto the same two
|
|
114
115
|
services, `CatalogService` and `InvocationService`. That is the point: a
|
|
@@ -129,6 +130,18 @@ settings win, and connecta warns the fallback was ignored. Invalid bounds throw
|
|
|
129
130
|
at construction, because a pool that quietly became unbounded is worse than a
|
|
130
131
|
deployment that refuses to boot. The queue is global FIFO across identities: a
|
|
131
132
|
capacity boundary, not tenant fairness, and one deployment serves one tenant.
|
|
133
|
+
`admission.requests.maxDurationMs` defaults to 300,000 ms and may be set to a
|
|
134
|
+
positive whole number up to 2,147,483,647 ms. Its clock starts when admission
|
|
135
|
+
grants a permit and covers authorization, tool execution, and response delivery.
|
|
136
|
+
The owning request aborts its derived signal at that deadline, including signals
|
|
137
|
+
passed to connector calls, and closes a live response. If workerd ends the
|
|
138
|
+
request without running that timer or cancellation, the controller reclaims
|
|
139
|
+
only the expired permit's scalar record on the next request (or a queued
|
|
140
|
+
request's own timer). It never reads the old request's signal or touches its
|
|
141
|
+
stream. Expired queued waiters are pruned before any handoff; every handed-off
|
|
142
|
+
permit has its own deadline even if the recipient never resumes. Lease identities
|
|
143
|
+
make an old late release harmless to a newer permit. Set the bound above the
|
|
144
|
+
longest admitted request the deployment expects to serve.
|
|
132
145
|
|
|
133
146
|
Call admission (`src/call-admission.ts`) answers what the envelope cannot see —
|
|
134
147
|
a connector's optional policy over its own `Connector.callTool` attempts,
|
|
@@ -139,19 +152,85 @@ rule refuses would charge a call that never reached the provider, the exact
|
|
|
139
152
|
accounting error a budget exists to prevent. Both layers are pinned by
|
|
140
153
|
`test/request-admission.test.ts` and `test/call-admission.test.ts`.
|
|
141
154
|
|
|
155
|
+
### Production Worker disconnects
|
|
156
|
+
|
|
157
|
+
The original production probe for [#573](https://github.com/zackbart/connecta/issues/573)
|
|
158
|
+
ran on 2026-09-25 with compatibility date `2025-01-01`, first with no flags and
|
|
159
|
+
then with `enable_request_signal`. It used Wrangler 4.114.0 and a Node 26.9.0
|
|
160
|
+
client. A raw Worker stream established the runtime behavior; a custom auth
|
|
161
|
+
provider returning a synthetic streaming 401 exercised Connecta's real admission
|
|
162
|
+
wrapper. Each case had its own Connecta instance with one request permit and no
|
|
163
|
+
queue. Health checks and subsequent MCP requests confirmed the same isolate ID.
|
|
164
|
+
The [recorded observations](https://github.com/zackbart/connecta/blob/a0ac904513fac735582b4bc9c817a48300134328/scripts/probes/worker-disconnect-2026-09-25.json)
|
|
165
|
+
contain all twelve cases, including served flags and client-end timing.
|
|
166
|
+
|
|
167
|
+
| Response and flag | Raw stream abort / cancel | Connecta source abort / cancel | Admission after response; next request |
|
|
168
|
+
| --- | --- | --- | --- |
|
|
169
|
+
| Heartbeats, no flag | neither | neither | active 1; 503 |
|
|
170
|
+
| Idle with a pending timer, no flag | neither | neither | active 1; 503 |
|
|
171
|
+
| Heartbeats, request signal enabled | abort only | both | active 0; 200 |
|
|
172
|
+
| Idle with a pending timer, request signal enabled | abort only | both | active 0; 200 |
|
|
173
|
+
| One chunk, then no timer or I/O, either configuration | neither | neither | active 1; 503 |
|
|
174
|
+
|
|
175
|
+
The last row is a different failure. The client observed the response end
|
|
176
|
+
*before* its planned disconnect, although the source never closed itself.
|
|
177
|
+
The runtime ended the response without running JavaScript cleanup. This does
|
|
178
|
+
not show a missing abort on a still-live request. The other cases remained open
|
|
179
|
+
until the client aborted. Admission was sampled 1.5 seconds later, followed by
|
|
180
|
+
an actual MCP request; this measures retained capacity, not an infinite leak.
|
|
181
|
+
|
|
182
|
+
Enable `enable_request_signal` when deploying on Workers: it let Connecta's
|
|
183
|
+
abort listener cancel the source and release admission for live responses in
|
|
184
|
+
this experiment. It does not solve runtime-ended streams. The admitted-request
|
|
185
|
+
bound above recovers capacity on the first later request at or after its
|
|
186
|
+
deadline, even if the old request's timer never ran. This is capacity recovery,
|
|
187
|
+
not proof that workerd cancelled an old stream. A still-live request reaches
|
|
188
|
+
the same hard deadline and is aborted in its own context.
|
|
189
|
+
Cloudflare documents the flag in its
|
|
190
|
+
[Request API](https://developers.cloudflare.com/workers/runtime-apis/request/).
|
|
191
|
+
|
|
192
|
+
A second production run on the same date tested the request bound at 2,000 ms.
|
|
193
|
+
All twelve raw-stream and MCP cases passed with both flag configurations. Live
|
|
194
|
+
MCP requests still rejected a competing request with 503 before the deadline;
|
|
195
|
+
the first MCP request after the deadline returned all eight tools with 200 in
|
|
196
|
+
the original isolate. Admission then reported zero active requests. Without the
|
|
197
|
+
flag, the source still recorded no abort or cancellation, so recovery did not
|
|
198
|
+
rely on runtime cleanup. The [recovery observations](https://github.com/zackbart/connecta/blob/32d8e94/scripts/probes/worker-disconnect-recovered-2026-09-25.json)
|
|
199
|
+
also retain two inconclusive queue-handoff cases: concurrent client connections
|
|
200
|
+
reached different isolates and could not establish an abandoned queued waiter.
|
|
201
|
+
A [second trial using HTTP/2](https://github.com/zackbart/connecta/blob/b174812/scripts/probes/worker-disconnect-h2-2026-09-25.json)
|
|
202
|
+
repeated the twelve passing core cases and verified queued cancellation with
|
|
203
|
+
request signals enabled. Even on one HTTP/2 connection, the no-signal queue case
|
|
204
|
+
crossed isolates and remained inconclusive. The Node and workerd regression
|
|
205
|
+
suites cover expired waiters, orphaned handoffs, and identity-safe late release
|
|
206
|
+
deterministically.
|
|
207
|
+
|
|
208
|
+
To repeat from a repository checkout, run
|
|
209
|
+
`node scripts/probes/worker-disconnect.mjs` with an authenticated Wrangler.
|
|
210
|
+
The script is not in the npm package. It deploys a disposable Worker containing
|
|
211
|
+
only synthetic data, verifies the served configuration, saves observations under `eval/results/`,
|
|
212
|
+
and deletes the Worker in `finally`. It uses no `waitUntil`, which would change
|
|
213
|
+
the lifetime being measured. The production runtime build number was not
|
|
214
|
+
exposed by the deployment; these findings are tied to the date and flags above.
|
|
215
|
+
|
|
142
216
|
## Storage, credentials, and connectors
|
|
143
217
|
|
|
144
218
|
`KVStorage` is `get`/`set`/`delete` with optional `list(prefix)`; core uses it for
|
|
145
219
|
connector state, catalogs, and result paging — a 15-minute TTL with one
|
|
146
220
|
runtime-wide accounting of stash bytes and entries, where a full stash returns the
|
|
147
221
|
successful call's preview and a paging-unavailable notice rather than a result id.
|
|
148
|
-
A second optional method, `compareAndSet(key, expected, next)`, is an
|
|
149
|
-
claim: `null` means absent (expired counts) on the way in and delete on
|
|
150
|
-
out.
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
222
|
+
A second optional method, `compareAndSet(key, expected, next, options?)`, is an
|
|
223
|
+
atomic claim: `null` means absent (expired counts) on the way in and delete on
|
|
224
|
+
the way out. A successful write accepts the same optional `ttlSeconds` as
|
|
225
|
+
`set`. Resumable writes require it: of two `resume_execution` calls racing
|
|
226
|
+
for one paused run exactly one may win, and every write that run sends is
|
|
227
|
+
marked on its header first, so a read and a write standing in for the claim
|
|
228
|
+
could send an approved write twice. Omitted, `execute.resumableWrites` is on
|
|
229
|
+
exactly when the store has it, and asking for it over a store without it
|
|
230
|
+
refuses to construct. Downstream OAuth uses it where the store has it, so
|
|
231
|
+
resealing legacy plaintext and discarding a refused grant cannot overwrite a
|
|
232
|
+
consent that landed in between, and falls back to a read and a write where it
|
|
233
|
+
does not.
|
|
155
234
|
Adapters: `src/storage/memory.ts` and `src/storage/file.ts` (Node) both provide
|
|
156
235
|
it, and the namespaced views core hands connectors forward it only when the
|
|
157
236
|
underlying store has it. `examples/worker/` carries two more: Cloudflare KV,
|
|
@@ -184,9 +263,9 @@ change who owns its auth.
|
|
|
184
263
|
|
|
185
264
|
## Optional deployment modules
|
|
186
265
|
|
|
187
|
-
`createConnecta` takes closed typed `ui`, `vault`, and `
|
|
188
|
-
factories at `/ui`, `/credentials`,
|
|
189
|
-
`/auth/bearer`. Root exports the contracts, never the implementations, and there
|
|
266
|
+
`createConnecta` takes closed typed `ui`, `vault`, `activity`, and `artifacts`
|
|
267
|
+
slots, with factories at `/ui`, `/credentials`, `/activity`, and `/artifacts`
|
|
268
|
+
and bearer auth at `/auth/bearer`. Root exports the contracts, never the implementations, and there
|
|
190
269
|
is no module array, runtime registration, or plugin lifecycle. Core keeps
|
|
191
270
|
discovery, the executor contract, invocation, permissions, and OAuth callback
|
|
192
271
|
verification; an omitted module contributes no runtime work at all.
|
|
@@ -202,17 +281,110 @@ provider leaves the other connections usable. A connector's status message
|
|
|
202
281
|
never reaches the page: it can quote a downstream error body, and that body can
|
|
203
282
|
quote the secret it rejected, so the details payload carries only a classified
|
|
204
283
|
`problem` — which picks fixed on-screen copy and a fixed fix prompt — and the raw
|
|
205
|
-
text goes to the server log.
|
|
284
|
+
text goes to the server log. The OAuth and credential Test notices keep the
|
|
285
|
+
same rule: those routes answer a downstream's failure in fixed words (a Test
|
|
286
|
+
answers only `{ ok }`), log its text, and the page picks a sentence by outcome
|
|
287
|
+
without ever rendering what the server sent. Credential handoff URLs exist only while the UI is
|
|
206
288
|
mounted; OAuth callbacks never need it.
|
|
207
289
|
|
|
208
|
-
Its appearance is one token layer
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
290
|
+
Its appearance is one token layer, and it is not the UI's alone: every page
|
|
291
|
+
connecta shows a person — the operator shell, the OAuth callback, a browser's
|
|
292
|
+
404, the artifact frame and Markdown pages — reads it. `src/operator-ui/tokens.css`
|
|
293
|
+
resolves every color, radius, and font through a custom property and mixes the
|
|
294
|
+
rest from those with `color-mix`; `page.css` holds the base typography and the
|
|
295
|
+
primitives (shell, masthead, buttons, badges, messages, the one-message status
|
|
296
|
+
page). The UI's `browser.css` imports both, and the build writes the same CSS,
|
|
297
|
+
minified, into `src/page-styles.ts`, a module of two strings that core imports
|
|
298
|
+
without the UI bundle, so an OAuth callback still renders with the UI omitted.
|
|
299
|
+
`renderPage` in `src/branding.ts` is the one layout: head, favicon (the default
|
|
300
|
+
is linked only when the UI serves it), `data-scheme`, the tokens with the theme
|
|
301
|
+
after them, and the masthead. So `branding.theme` only has to append a `:root`
|
|
302
|
+
block after the stylesheet. The five tokens it accepts are gated in
|
|
303
|
+
`src/branding.ts`, each by a narrow syntactic check: deployment config reaches a
|
|
304
|
+
`<style>` element here, and an unvalidated value would be CSS injection. The
|
|
305
|
+
dark palette is the same tokens under `prefers-color-scheme`; `colorScheme` pins
|
|
306
|
+
one with a `data-scheme` attribute on the page, and the artifact viewer passes
|
|
307
|
+
the same attribute into a Markdown page, never into authored HTML.
|
|
308
|
+
|
|
309
|
+
The artifacts module — `artifacts()` from `/artifacts`, implemented in
|
|
310
|
+
`src/artifacts/` — is the one module that contributes a connector. Core
|
|
311
|
+
appends its prebuilt `artifacts` connector to the configured set, so its reads
|
|
312
|
+
and writes take the same catalog, invocation, admission, and activity paths as
|
|
313
|
+
any other connector; the slot binds its optional refresh runner once, after the
|
|
314
|
+
registry and executor exist, and a deployment
|
|
315
|
+
without it has no connector, no guide, and no artifact storage traffic. Its
|
|
316
|
+
writes are not read-only anywhere — `call_tool` refuses them and discovery
|
|
317
|
+
lists them approval-required — but they skip approval inside programs through
|
|
318
|
+
the connector's own `approval: "never"`, which `execute.approval` switches off,
|
|
319
|
+
because every write is a new immutable version anyone can roll back. The
|
|
320
|
+
module needs `publicUrl`: the links it hands out are shared, so they must never
|
|
321
|
+
come from a request's `Host`.
|
|
322
|
+
|
|
323
|
+
An artifact can version an `execute_code` program and target data document with
|
|
324
|
+
`set_refresh`, on a manual, daily, or weekly schedule. The deployment calls
|
|
325
|
+
`artifacts.runDue()` from a Node timer or Worker `scheduled()` handler; core
|
|
326
|
+
starts no background jobs. One tick scans at most 1,000 heads and starts at
|
|
327
|
+
most 10 due runs. Every run uses the configured executor, host-call budget,
|
|
328
|
+
and watchdog, with a registry view containing only shared connectors. The
|
|
329
|
+
owner admitted to `set_refresh` is stored with the program. Every run rechecks
|
|
330
|
+
that identity's current `connectorAccess` and pool grant, including continued
|
|
331
|
+
access to `artifacts.set_refresh`, then intersects those grants with shared
|
|
332
|
+
connectors. Revoking either grant stops later runs.
|
|
333
|
+
Resumable writes and approval exemptions are off. A refused call fails the
|
|
334
|
+
entire refresh even if its program catches the error. The head CAS claims one
|
|
335
|
+
run per artifact; the data commit checks the claim ID and program version in
|
|
336
|
+
the same CAS. The claim deadline starts before executor admission and aborts
|
|
337
|
+
a queued or active play when it expires. An expired old run cannot publish over
|
|
338
|
+
a newer claim. Run history retains 50 records with bounded logs. A failure leaves the
|
|
339
|
+
last good data intact and marks the artifact stale. Library and viewer show
|
|
340
|
+
only outcome enums and times, never downstream messages or logs.
|
|
341
|
+
|
|
342
|
+
With the operator UI mounted, `/artifacts` lists the team pages and
|
|
343
|
+
`/artifacts/<id>` opens one. The shell contains no page data. It reads the
|
|
344
|
+
artifact through an authenticated JSON route with the operator UI's inbound
|
|
345
|
+
identity and connector visibility, then passes the document into a sandboxed
|
|
346
|
+
frame. Snapshot links pin the view and document versions. The frame's response
|
|
347
|
+
CSP gives scripts an opaque origin, refuses fetch, XHR, images and other
|
|
348
|
+
unlisted requests. External script, style, and font origins default to none;
|
|
349
|
+
a deployment opts in to each one it trusts with page data. The shell tightens
|
|
350
|
+
its own `frame-src` to `'none'` after the fixed bootstrap loads and before it
|
|
351
|
+
hands over the page,
|
|
352
|
+
so a page script cannot navigate even its own frame to send data in a URL.
|
|
353
|
+
This browser boundary is covered by Chromium tests, including a
|
|
354
|
+
`document.open/write` rewrite.
|
|
355
|
+
|
|
356
|
+
`artifactOrigin` optionally names a distinct HTTPS origin for these pages.
|
|
357
|
+
The main host redirects every `/artifacts` path to it; the artifact host
|
|
358
|
+
answers every other path, including `/mcp` and operator APIs, with 404. The
|
|
359
|
+
artifact shell still uses the same inbound auth providers and checks the
|
|
360
|
+
identity on each data read. The bare shell and fixed frame bootstrap carry no
|
|
361
|
+
artifact data, so a visitor can reach sign-in before an authenticated read.
|
|
362
|
+
Bearer tokens in browser localStorage belong to one origin: signing in on the
|
|
363
|
+
main host does not sign in on the artifact host. A redirected visitor enters
|
|
364
|
+
the token again there; the redirect carries no credential. Cookie or session
|
|
365
|
+
auth likewise depends on the provider admitting that separate origin.
|
|
366
|
+
|
|
367
|
+
Storage is an `ArtifactStore`, and `kvArtifactStore` is the reference: over any
|
|
368
|
+
`KVStorage` with `compareAndSet` and `list`, one head record per artifact is
|
|
369
|
+
the only key ever compared-and-set, earlier versions sit beside it immutable,
|
|
370
|
+
and bodies are content-addressed — in the key-value store, or in R2 through the
|
|
371
|
+
Worker example's `r2-artifact-blobs.ts`. Workers KV is refused at construction,
|
|
372
|
+
because a write that cannot swap its head can lose a teammate's edit. Every
|
|
373
|
+
rule lives once, in `src/artifacts/operations.ts`: a write checks its base
|
|
374
|
+
against the one stream it touches, so of two writes from the same base exactly
|
|
375
|
+
one wins and the other is a `conflict` carrying a bounded `current` map, while
|
|
376
|
+
a head that moved for an unrelated reason is re-read and retried rather than
|
|
377
|
+
reported. Patches match exactly once or change nothing, rollback is a new
|
|
378
|
+
version reusing an old body, and the limits are checked on every write.
|
|
379
|
+
Validation is a hand-written tokenizer and CommonMark subset rather than a
|
|
380
|
+
dependency, because it is a lint: the page's CSP is the boundary.
|
|
381
|
+
|
|
382
|
+
Who made a version is the one fact a built-in connector needs that
|
|
383
|
+
`ConnectorContext` deliberately does not carry. `src/connector-caller.ts`
|
|
384
|
+
attaches the admitted identity beside the context on the request's scoped
|
|
385
|
+
view, the way the OAuth sealer rides beside it — readable only by in-repo
|
|
386
|
+
code, set from the authorization and never from arguments
|
|
387
|
+
(`test/identity-scope.test.ts`, `test/artifacts-connector.test.ts`).
|
|
216
388
|
|
|
217
389
|
## Import-graph purity
|
|
218
390
|
|
|
@@ -306,12 +478,13 @@ would be another partition's.
|
|
|
306
478
|
|
|
307
479
|
| Area | Shape |
|
|
308
480
|
| --- | --- |
|
|
309
|
-
| Requests (`src/server.ts`, `src/routes/mcp.ts`, `src/routes/oauth.ts`) | One fiber per `fetch`, tied to `request.signal`. An `/mcp` request's admission permit and every `McpServer` it builds live in a Scope the response carries out, closed when the body ends, fails, or is cancelled, when the signal aborts, or at once if the handler fails first. The OAuth callback is uninterruptible: a single-use code's exchange and catalog invalidation are one commitment. |
|
|
481
|
+
| Requests (`src/server.ts`, `src/routes/mcp.ts`, `src/routes/oauth.ts`) | One fiber per `fetch`, tied to `request.signal`. An `/mcp` request's admission permit and every `McpServer` it builds live in a Scope the response carries out, closed when the body ends, fails, or is cancelled, when the signal aborts, when its admitted-request lifetime ends in that request, or at once if the handler fails first. If workerd runs none of those callbacks, the next request reclaims only the expired admission record; it cannot close another request's Scope. The OAuth callback is uninterruptible: a single-use code's exchange and catalog invalidation are one commitment. |
|
|
310
482
|
| Admission (`executor-admission.ts`, `call-admission.ts`) | Queued waiters are Deferreds settled by whoever removes them from the queue; a wait is one flat race of grant, Clock timeout, and signal. The uncontended path stays synchronous. Two controllers, as [#453](https://github.com/zackbart/connecta/issues/453) requires. |
|
|
311
483
|
| One tool call (`invocation.ts`) | One fiber: resolution, the read-only and schema refusals, admission, and the downstream attempt sit under a single `withDeadlineEffect`, whose expiry interrupts the call wherever it is. The permit is an `acquireRelease`; the connector call is `Effect.tryPromise` over the unchanged `Connector`. |
|
|
312
484
|
| `execute_code` (`execute.ts`) | One fiber whose Scope owns the run's signal and executor lease, so a result, a throw, the watchdog, and cancellation all release the lease and abort the signal the same way. The executor's `acquire()` and `execute()` stay Promises raced against the signal and `execute.watchdogMs`. Each guest host call is a fiber of its own. |
|
|
313
|
-
|
|
|
314
|
-
|
|
|
485
|
+
| Resumable writes (`resumable.ts`) | A play's stop is a Deferred its write gate completes, raced against the executor like the watchdog; the gate for call n awaits the decision Deferreds of every lower-numbered call; header writes are one compare-and-set at a time on a per-play turn. A paused run holds nothing request-bound — its journal is data, and the next play is a new request's. |
|
|
486
|
+
| Discovery (`catalog-service.ts`) | A request-scoped cache: one shared read per connector (`runtime/shared-read.ts`), settled by the read itself and carrying its own signal and the probe timeout whichever asker starts it. Each asker waits under its own deadline and signal, so one that times out or is cancelled fails alone, and the read is cancelled only once every asker has gone. Fan-out is `Effect.forEach` under the discovery concurrency. |
|
|
487
|
+
| Registry (`registry.ts`) | Catalog persistence and the result stash are programs over `Storage`. A refresh flight is a Deferred its publishing request completes, bounded by its owner's deadline (the default probe timeout when it has none); persisted-catalog writes take per-connector turns, each a Deferred its own request completes. Same-request loads share one read the way discovery's do. |
|
|
315
488
|
| Remote MCP (`connectors/remote-mcp.ts`) | Each request scope's state holds a Scope, each connection is a lease forked from it, and a connect in flight is a Deferred carrying the client it connected. Closing a session and the transport are each bounded to a second. |
|
|
316
489
|
| Downstream OAuth (`auth/downstream-oauth.ts`) | A refresh flight is a Deferred; the owner's redemption is a fiber its abort interrupts, and committing an answer that already exists is uninterruptible. |
|
|
317
490
|
| Operator and activity data (`routes/operator.ts`) | Each JSON route is one program run by `serveOperator` behind the Promise `handle()`. Reads run under the request's signal; writes do not, so a vault write or OAuth disconnect that started reaches its cache invalidation. |
|
|
@@ -368,6 +541,20 @@ when its only pending work is waiting on another request and it has no timer or
|
|
|
368
541
|
I/O of its own. Connecta's deadline timers are what keep a waiting request
|
|
369
542
|
alive and bounded, so no wait may lose its timer.
|
|
370
543
|
|
|
544
|
+
**A shared flight has a lifetime of its own.** A catalog refresh flight runs in
|
|
545
|
+
its owner's request, under its owner's scope and signal, and lives only as long
|
|
546
|
+
as its owner's deadline. Past that the owner may have answered and gone, taking
|
|
547
|
+
its I/O with it on workerd, and a connector that ignores its abort signal would
|
|
548
|
+
otherwise hold every later reader on a flight nothing will ever settle. So each
|
|
549
|
+
joiner waits on its own timer, set to the flight's bound: when it fires, the
|
|
550
|
+
joiner abandons the flight and makes a fresh attempt, and a reader that arrives
|
|
551
|
+
after the bound starts one without joining. Joiners also leave when the owner
|
|
552
|
+
fails for a reason that was only its own, such as its cancellation, rather than
|
|
553
|
+
inheriting it. A flight's result enters the caches only while the flight is
|
|
554
|
+
current, checked at the same points as the catalog generation, so a stuck
|
|
555
|
+
listing that lands late is never cached or persisted and never overwrites the
|
|
556
|
+
fresh attempt's.
|
|
557
|
+
|
|
371
558
|
### Why not Schema or HttpApi
|
|
372
559
|
|
|
373
560
|
Both were measured and both lost. `effect` is imported through its barrel,
|
package/documentation/auth.md
CHANGED
|
@@ -51,8 +51,11 @@ deployments and providers that return no identity share one result partition.
|
|
|
51
51
|
|
|
52
52
|
`identity.connectorAccess` returns `"all"` — the default — or a list of grants:
|
|
53
53
|
a declared connector id opens every tool on it, a `connector.tool` address opens
|
|
54
|
-
that tool alone, and
|
|
55
|
-
|
|
54
|
+
that tool alone, and `{ tool: "connector.tool", requireReadOnly: true }` opens
|
|
55
|
+
that exact tool only while the loaded catalog says it is explicitly read-only.
|
|
56
|
+
The guarded form accepts no connector-wide grant. Grants are additive, so a
|
|
57
|
+
bare id or an unrestricted address beside a guarded address wins for that
|
|
58
|
+
tool. It governs discovery and use alike.
|
|
56
59
|
|
|
57
60
|
Tool grants are enforced in the scoped registry view, below the catalog service,
|
|
58
61
|
so `search_tools`, both call tools, a program's `connecta.search`,
|
|
@@ -69,6 +72,84 @@ There is no *caller-selected* tool set. A narrower slice is a branch in this
|
|
|
69
72
|
resolver or a config-declared pool; a bot that needs its own slice is its own
|
|
70
73
|
bearer subject. What a request may never do is name its own scope.
|
|
71
74
|
|
|
75
|
+
### Team read sets
|
|
76
|
+
|
|
77
|
+
`connectorAccess` is the supported way to give Engineering and Marketing
|
|
78
|
+
different views of one deployment. Keep membership in deployment code and
|
|
79
|
+
review the exact tool addresses you intend to grant:
|
|
80
|
+
|
|
81
|
+
```ts
|
|
82
|
+
const engineering = new Set(["engineer-id"]);
|
|
83
|
+
const marketing = new Set(["marketer-id"]);
|
|
84
|
+
const engineeringGrants = ["issues", { tool: "campaigns.list", requireReadOnly: true }] as const;
|
|
85
|
+
const marketingGrants = ["campaigns", { tool: "issues.search", requireReadOnly: true }] as const;
|
|
86
|
+
|
|
87
|
+
createConnecta({
|
|
88
|
+
auth: cloudflareAccessAuth(),
|
|
89
|
+
identity: {
|
|
90
|
+
connectorAccess: ({ principal }) => {
|
|
91
|
+
const id = principal?.id;
|
|
92
|
+
if (!id) return [];
|
|
93
|
+
return [...new Set([
|
|
94
|
+
...(engineering.has(id) ? engineeringGrants : []),
|
|
95
|
+
...(marketing.has(id) ? marketingGrants : []),
|
|
96
|
+
])];
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
connectors: [issues, campaigns, billing],
|
|
100
|
+
executor,
|
|
101
|
+
});
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Here Engineering sees every `issues` tool and only `campaigns.list`;
|
|
105
|
+
Marketing sees every `campaigns` tool and only `issues.search`. Neither sees
|
|
106
|
+
`billing`. Someone in both groups gets both connectors in full because grants
|
|
107
|
+
are additive. An unknown or unprincipalled identity gets an empty view. The
|
|
108
|
+
example assumes the two exact tools have been reviewed as read-only; the names
|
|
109
|
+
themselves carry no safety meaning. The guarded grants also check the current
|
|
110
|
+
loaded annotations before discovery or use. Use a stable authenticated
|
|
111
|
+
principal or subject for membership, never a request header or tool argument. A pool can
|
|
112
|
+
narrow any of these views on its endpoint, but cannot make plain `/mcp` narrower.
|
|
113
|
+
|
|
114
|
+
New tool names are excluded until added to a reviewed list. A removed or
|
|
115
|
+
renamed name is unreachable and warned when the scoped view reads its catalog.
|
|
116
|
+
For a guarded grant, missing, false, or contradictory read-only annotations
|
|
117
|
+
also remove that tool from discovery and every invocation path, including
|
|
118
|
+
`call_destructive_tool` and approval-exempt programs. An unrestricted exact
|
|
119
|
+
string grant has the earlier behavior: a tool that changes from read to write
|
|
120
|
+
stays granted, though `call_tool` refuses it and the write paths take over.
|
|
121
|
+
Changing schema alone does not revoke either kind of grant. Review schemas,
|
|
122
|
+
annotations, and downstream behavior before changing lists or upgrading a
|
|
123
|
+
maintained provider. An arbitrary remote MCP catalog can drift without a
|
|
124
|
+
package upgrade; a valid stale cached catalog may keep its earlier
|
|
125
|
+
classification until refresh. A downstream can also keep a read-only
|
|
126
|
+
annotation while changing the behavior behind it. For `api()` tools, the
|
|
127
|
+
deployment author owns both declaration and handler. Connecta enforces the
|
|
128
|
+
loaded declaration, not the downstream's promise of no side effects. A
|
|
129
|
+
restricted downstream credential adds protection where the provider offers
|
|
130
|
+
one, but does not replace the per-identity grant.
|
|
131
|
+
|
|
132
|
+
When the resolver returns `[]`, connector-level discovery and the connection
|
|
133
|
+
UI have no connectors to show. An address-only grant can leave a connector
|
|
134
|
+
visible while its named tool is absent; the tool stays unreachable. A failed
|
|
135
|
+
remote catalog load is an error, not an empty successful catalog; a valid stale
|
|
136
|
+
catalog may be served within its configured stale window. A guarded grant
|
|
137
|
+
whose tool loses qualification can leave its connector visible with no tools.
|
|
138
|
+
Personal OAuth ownership and credential-administration permissions remain separate from
|
|
139
|
+
visibility. These are the checks for a team read set:
|
|
140
|
+
|
|
141
|
+
| Boundary | Expected result for a reviewed address and an excluded write |
|
|
142
|
+
| --- | --- |
|
|
143
|
+
| `search_tools`, `describe_tools`, connector guides | Show the granted address; omit the write. |
|
|
144
|
+
| `call_tool` | Invoke an explicitly read-only granted address; return `unknown_tool` for the write. |
|
|
145
|
+
| `call_destructive_tool` | Return `unknown_tool` for the excluded write, without dispatch. |
|
|
146
|
+
| `execute_code` search, describe, call | See granted tools only; refuse the excluded write. A granted write still follows the separate approval rules. |
|
|
147
|
+
| Connection UI | List granted tools only; never turn visibility into credential administration. |
|
|
148
|
+
| `/mcp/<pool>` | Intersect its grants with the identity view; plain `/mcp` keeps the identity view. |
|
|
149
|
+
|
|
150
|
+
Integration tests in `test/identity-scope.test.ts` exercise these paths and
|
|
151
|
+
the older unrestricted exact-grant behavior.
|
|
152
|
+
|
|
72
153
|
## Pools
|
|
73
154
|
|
|
74
155
|
A pool is a named slice of the deployment served at its own endpoint,
|
|
@@ -169,6 +250,87 @@ means authorizing again. A vault without the optional `seal`/`open` members
|
|
|
169
250
|
keeps these values plaintext and draws a startup warning. Without any vault,
|
|
170
251
|
nothing changes: the state is plaintext, as it always was.
|
|
171
252
|
|
|
253
|
+
## Starting, restarting, and retiring an OAuth epoch
|
|
254
|
+
|
|
255
|
+
Every downstream OAuth value lives under an **epoch**, the value of
|
|
256
|
+
`oauth:generation`. A restart or disconnect publishes a new one before it
|
|
257
|
+
deletes anything, and a write from a flow that captured an older epoch lands
|
|
258
|
+
in that epoch's own key namespace, where no reader looks. The fence is what
|
|
259
|
+
keeps a retired grant out of use; deleting the old keys is hygiene on top.
|
|
260
|
+
|
|
261
|
+
The operator's `POST /ui/oauth/<id>` takes a `mode` query parameter:
|
|
262
|
+
|
|
263
|
+
| Request | What it does |
|
|
264
|
+
| --- | --- |
|
|
265
|
+
| `POST /ui/oauth/<id>` or `?mode=restart` | `startAuth(ctx, { force: true })`: a new epoch, with tokens, the registered client, discovery, and any pending flow wiped, so the next flow registers a client again. |
|
|
266
|
+
| `?mode=continue` | `startAuth(ctx, { force: false })`: hand back the pending authorization URL if it was written in the last ten minutes; otherwise start a flow in the current epoch, reusing the stored client registration. A disconnected connector still gets its new epoch first. |
|
|
267
|
+
| `DELETE /ui/oauth/<id>` | Disconnect: a disconnected epoch that passive reads never turn back into a consent flow. |
|
|
268
|
+
|
|
269
|
+
Any other `mode`, or more than one, is a 400 before anything starts; the
|
|
270
|
+
management permission, visibility, and principal checks are the same for
|
|
271
|
+
both modes, and a personal connector continues only the caller's own flow.
|
|
272
|
+
A 200 answers `{ state, authorizationUrl?, reused? }`, with `reused` present
|
|
273
|
+
beside every URL: `true` when an earlier start's URL came back unchanged. A
|
|
274
|
+
continue that reused a URL or found the connection healthy changed nothing,
|
|
275
|
+
so it leaves the cached catalog alone; every other start invalidates it. The
|
|
276
|
+
ten minutes are `PENDING_AUTHORIZATION_MAX_AGE_MS`. Connecta never expires
|
|
277
|
+
its own half of a URL, but authorization servers expire theirs, and a fresh
|
|
278
|
+
start that keeps the registration costs one authorization request. A URL
|
|
279
|
+
stored without a write time — by an earlier release, or by a connector never
|
|
280
|
+
reset since epochs arrived — is stale by definition. `authorize_connector`
|
|
281
|
+
without `force` follows the same rule.
|
|
282
|
+
|
|
283
|
+
Continue trusts the stored registration, which carries the `redirect_uris`
|
|
284
|
+
it was registered with. After a `publicUrl` change the authorization server
|
|
285
|
+
refuses that client's new callback at consent. Restart registers a client
|
|
286
|
+
for the current URL and recovers.
|
|
287
|
+
|
|
288
|
+
A restart cannot know which retired epochs a late write reached, so it
|
|
289
|
+
publishes a **cleanup lineage** under the new epoch before activating it:
|
|
290
|
+
the retired epochs, as the same plain list of names every release reads, and
|
|
291
|
+
beside it a record of when each retired. A published lineage is never
|
|
292
|
+
rewritten. A stale writer whose own cleanup fails appends to the live
|
|
293
|
+
lineage, and a restart that rewrote the list it had read could drop that
|
|
294
|
+
append. An epoch leaves the lineage only by being left out of its
|
|
295
|
+
successor's.
|
|
296
|
+
|
|
297
|
+
A restart does the same storage work however many came before it. After the
|
|
298
|
+
fence it deletes the one epoch it retired: six values, then that epoch's own
|
|
299
|
+
lineage records. An epoch's manifest outlives its values only when their
|
|
300
|
+
deletion failed, so the restart also reads the manifests of the eight most
|
|
301
|
+
recently retired epochs (`RETRY_PROBES`) and cleans up again any that still
|
|
302
|
+
have one. A Disconnect or Restart that reported a failed cleanup therefore
|
|
303
|
+
deletes the old grant when the operator retries it. Before publishing, it
|
|
304
|
+
sweeps up to 16 epochs retired more than `CLEANUP_GRACE_MS` (24 hours) ago,
|
|
305
|
+
oldest first, and leaves out only those whose values and records were all
|
|
306
|
+
deleted. A failed sweep is carried and tried again later, and never fails
|
|
307
|
+
the restart. Any other epoch inside its grace is not deleted again. A late write into it is unreadable behind the fence,
|
|
308
|
+
and the late writer deletes it itself. If that delete fails, the writer
|
|
309
|
+
records the epoch again as retired at that moment, appending it or, when it
|
|
310
|
+
is already listed, moving only its time. A restart that swept it re-reads the
|
|
311
|
+
lineage before publishing and keeps it. Whatever remains is swept once the
|
|
312
|
+
grace has passed. Two such writers racing can lose one time update, which
|
|
313
|
+
leaves the earlier time; a time missing altogether reads as retired at the
|
|
314
|
+
moment of the restart that reads it, which only lengthens a grace.
|
|
315
|
+
|
|
316
|
+
**The assumption:** no request holds a retired epoch for a day. The writers
|
|
317
|
+
that can land late are OAuth flows, refreshes, and readers whose Workers KV
|
|
318
|
+
replica still serves the old generation for a minute or more; none of them
|
|
319
|
+
run that long. If it fails, only a writer that dies between its write and
|
|
320
|
+
its own cleanup leaves residue no restart tracks, and that residue is never
|
|
321
|
+
readable.
|
|
322
|
+
|
|
323
|
+
The lineage holds at most 5,000 epochs, the ones retired within the last day
|
|
324
|
+
plus any the sweep has not reached. A restart that would exceed that is
|
|
325
|
+
refused before the fence moves, which takes more than 4,000 restarts of one
|
|
326
|
+
connector in a day on top of the 1,000 an earlier release allowed. A
|
|
327
|
+
connector that reached that release's 1,000 wall restarts again the day it
|
|
328
|
+
upgrades, and its old entries drain 16 per restart a day later. An older
|
|
329
|
+
release ignores the times, so a rollback still restarts unless a lineage has
|
|
330
|
+
grown past its own 1,000 cap. Deletes run six at a time, the Workers limit
|
|
331
|
+
on simultaneous connections, and catalog invalidation deletes its chunks
|
|
332
|
+
concurrently under the registry's chunk I/O bound.
|
|
333
|
+
|
|
172
334
|
## Refresh failures
|
|
173
335
|
|
|
174
336
|
A failed token refresh means one of two things, and connecta decides which
|
|
@@ -348,6 +510,28 @@ instead of a base64 stack on every route.
|
|
|
348
510
|
|
|
349
511
|
## Human authentication management
|
|
350
512
|
|
|
513
|
+
An operator's OAuth Connect or Restart request gives the start and its handoff
|
|
514
|
+
30 seconds. The request's abort signal and that deadline reach downstream OAuth
|
|
515
|
+
fetches, including metadata discovery and dynamic registration. If the deadline
|
|
516
|
+
expires, the route returns `504` with `OAuth authorization start timed out` after
|
|
517
|
+
an already-started reset finishes. Storage has no cancellation contract: its
|
|
518
|
+
generation write must drain before a response, or it could publish an old epoch
|
|
519
|
+
over a newer flow. Catalog invalidation and connector scope close also finish
|
|
520
|
+
outside the deadline. Those storage and cleanup waits can extend the click past
|
|
521
|
+
30 seconds; the bound is on downstream work, where a hung authorization server
|
|
522
|
+
was the failure. The OAuth provider checks its signal before writing and removes
|
|
523
|
+
a cancelled value write that finishes late. An operator Disconnect keeps its
|
|
524
|
+
existing commit-through behavior even if the browser leaves.
|
|
525
|
+
|
|
526
|
+
Restart retains a dynamically registered client only when its stored issuer and
|
|
527
|
+
the connector's URL, redirect URI, client metadata, auth scope, transport settings,
|
|
528
|
+
and owner partition still match. It drops the grant and one-shot flow state,
|
|
529
|
+
selects the authorization server again, and re-seals the retained client under
|
|
530
|
+
the replacement epoch key. A different issuer registers a new client. Disconnect
|
|
531
|
+
and issuer-mismatch recovery discard it. The SDK has no client-validation
|
|
532
|
+
exchange while constructing a fresh consent URL; a provider that revokes a
|
|
533
|
+
retained client may refuse it only at consent or callback, after this request.
|
|
534
|
+
|
|
351
535
|
Credential and OAuth mutation require an admitted interactive human, connector
|
|
352
536
|
visibility, the appropriate shared or personal permission, and an exact
|
|
353
537
|
same-origin `Origin` for browser requests. A configured MCP bearer never becomes
|