@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
|
@@ -4,8 +4,10 @@ One Web-standard `fetch(request) => Promise<Response>` handler, a long-lived
|
|
|
4
4
|
registry behind it, and a strict rule about what may be imported. Everything
|
|
5
5
|
else in this repository is a detail of those three things.
|
|
6
6
|
|
|
7
|
-
Read [`ethos.md`](../ethos.md) first
|
|
8
|
-
|
|
7
|
+
Read [`ethos.md`](../ethos.md) first; this guide says how the shape it describes
|
|
8
|
+
is assembled and where each subsystem lives. The surface itself belongs to
|
|
9
|
+
[meta-tools](./meta-tools.md), [code mode](./code-mode.md), and
|
|
10
|
+
[inbound auth](./auth.md).
|
|
9
11
|
|
|
10
12
|
## The two lifetimes
|
|
11
13
|
|
|
@@ -13,234 +15,418 @@ Almost every bug in this codebase is a lifetime mistake, so the split is worth
|
|
|
13
15
|
stating before anything else.
|
|
14
16
|
|
|
15
17
|
**Per isolate, built once.** `createConnecta(config)` returns
|
|
16
|
-
`{ fetch, registry, close }
|
|
17
|
-
resolution, catalog caches, observed output schemas, connector
|
|
18
|
-
per-connector call limiters. It is
|
|
19
|
-
isolate
|
|
20
|
-
|
|
21
|
-
deployment shapes build it outside the request handler.
|
|
18
|
+
`{ fetch, registry, close }` (`src/index.ts`). The `Registry` owns the connector
|
|
19
|
+
set, address resolution, catalog caches, observed output schemas, connector
|
|
20
|
+
health, and the per-connector call limiters. It is built once and lives as long
|
|
21
|
+
as the isolate — on Workers a lazy module-scope singleton, which is why both
|
|
22
|
+
deployment shapes construct it outside the request handler.
|
|
22
23
|
|
|
23
24
|
An OAuth `remoteMcp()` connector also owns a runtime-local refresh completion
|
|
24
|
-
gate. It coordinates credential mutation across
|
|
25
|
-
|
|
26
|
-
cancel the owner.
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
flight, and when the owner fails after that response — cancelled, redirected
|
|
35
|
-
to authorization, or invalidated — it persists the rotation on the host's own
|
|
36
|
-
write, holds contenders behind the pending-mutation marker until that write
|
|
37
|
-
lands, and hands them the saved rotation. No contender ever redeems the retired
|
|
38
|
-
token again, and the marker can no longer outlive the write that clears it
|
|
39
|
-
([#526](https://github.com/zackbart/connecta/issues/526)).
|
|
40
|
-
|
|
41
|
-
The coordinator retains the owner's abort signal only through one temporary
|
|
42
|
-
listener on the exact active refresh. Save, failure, cancellation, or
|
|
43
|
-
generation retirement removes it along with the map entry. It never retains a
|
|
44
|
-
token response, client, or transport. If cancellation lands after a valid
|
|
45
|
-
response while its credential write is still running, a generation-keyed
|
|
46
|
-
identity marker rejects new owners until that exact write finishes. The marker
|
|
47
|
-
contains no promise and generation retirement removes it.
|
|
25
|
+
gate (`src/auth/downstream-oauth.ts`). It coordinates credential mutation across
|
|
26
|
+
concurrent request scopes while sharing no client, transport, or response, and
|
|
27
|
+
never lets a follower cancel the owner. The subtle part is that a valid token
|
|
28
|
+
response consumes the refresh token whether or not the owner survives to save it,
|
|
29
|
+
so the accepted tokens live on the flight: cancelling the owner *before* a valid
|
|
30
|
+
response fails the joiners, because promoting one could replay a token the
|
|
31
|
+
authorization server already consumed, while cancelling it *after* one does not —
|
|
32
|
+
the host persists the rotation on its own write, holds contenders behind a
|
|
33
|
+
generation-keyed pending-mutation marker until that write lands, and hands them
|
|
34
|
+
the saved rotation ([#526](https://github.com/zackbart/connecta/issues/526)).
|
|
48
35
|
|
|
49
36
|
**Per request, and no longer.** The MCP server, its transport, downstream MCP
|
|
50
37
|
clients, abort signals, and the connector scope a probe opens all belong to the
|
|
51
|
-
request that created them. `Nothing request-bound survives a request` is an
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
The registry is deliberately on the long side of that line and the MCP server
|
|
62
|
-
deliberately on the short side. A fresh `McpServer` per request is what makes
|
|
63
|
-
the deployment stateless: no sessions, no server push, no resumability, and
|
|
64
|
-
scope resolved from the request rather than remembered.
|
|
38
|
+
request that created them. `Nothing request-bound survives a request` is an ethos
|
|
39
|
+
invariant, not a style preference: a client retained across requests on Workers
|
|
40
|
+
is a cross-request capability leak, and a promise awaited after the response is
|
|
41
|
+
work the runtime may already have torn down. Deferred work has one sanctioned
|
|
42
|
+
channel, `ctx.waitUntil`, threaded through `fetch(request, env, ctx)` — activity
|
|
43
|
+
writes use it, as does a stale-window catalog refresh, which owns a fresh scope
|
|
44
|
+
and deadline rather than carrying the inbound one past the request. And a fresh
|
|
45
|
+
`McpServer` per request is what makes the deployment stateless: no sessions, no
|
|
46
|
+
server push, no stream resumability, scope resolved rather than remembered.
|
|
65
47
|
|
|
66
48
|
## Request lifecycle
|
|
67
49
|
|
|
68
|
-
`src/server.ts` is the composition root
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
read top to bottom.
|
|
50
|
+
`src/server.ts` is the composition root: MCP origin check, scheme upgrade, route
|
|
51
|
+
table, security headers. Route *order* is the contract — several routes would
|
|
52
|
+
behave differently if they were reachable in another order — so read the table
|
|
53
|
+
top to bottom.
|
|
73
54
|
|
|
74
55
|
| Order | Route | Notes |
|
|
75
56
|
| --- | --- | --- |
|
|
76
|
-
| 0 | MCP Origin check |
|
|
77
|
-
| 0 | HTTPS upgrade | 308 to
|
|
78
|
-
| 0 | Cloudflare Access (Worker
|
|
79
|
-
| 1 | Mounted UI routes |
|
|
80
|
-
| 2 | MCP preflight | Allowed `OPTIONS` on `/mcp
|
|
81
|
-
| 2 | Other `OPTIONS` | Auth metadata
|
|
57
|
+
| 0 | MCP Origin check | A disallowed `Origin` on `/mcp*` is a fixed 403 before redirects, admission, auth, or preflight — costing no permit and no auth lookup. Originless requests are admitted. |
|
|
58
|
+
| 0 | HTTPS upgrade | 308 to an HTTPS `publicUrl`, with path and query *assigned* onto it rather than resolved against it, so a `//host` pathname cannot replace the origin. `/health` is exempt: a loopback probe must not need public DNS. |
|
|
59
|
+
| 0 | Cloudflare Access (Worker, when enabled) | Edge admission ahead of this table; an admitted invocation carries trusted identity in `ctx.access`. |
|
|
60
|
+
| 1 | Mounted UI routes | Before wildcard OPTIONS, so mutation routes refuse preflight rather than inheriting MCP CORS. No UI module, no routes. |
|
|
61
|
+
| 2 | MCP preflight | Allowed `OPTIONS` on `/mcp*`: 204 without admission or auth. |
|
|
62
|
+
| 2 | Other `OPTIONS` | Auth metadata first, otherwise compatibility CORS preflight. |
|
|
82
63
|
| 3 | `/.well-known/*` | Auth metadata, or 404. |
|
|
83
|
-
| 4 | `/health` | Open payload-free health, executor, admission, and deployment metadata
|
|
84
|
-
| 5 | `/oauth/callback/<connectorId>` | Core downstream OAuth completion, state
|
|
85
|
-
| 6 | `/mcp`, `/mcp/<pool>` |
|
|
64
|
+
| 4 | `/health` | Open and payload-free: health, executor, admission, and deployment metadata, with drift as stable short hashes. |
|
|
65
|
+
| 5 | `/oauth/callback/<connectorId>` | Core downstream OAuth completion, state and personal-ownership checked, independent of the UI. |
|
|
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). |
|
|
86
67
|
| 7 | Other paths | 404. Custom HTTP routes belong to the deployment. |
|
|
87
68
|
|
|
69
|
+
Every response leaves through `withSecurityHeaders`, and the UI module adds a
|
|
70
|
+
nonce-based script CSP and framing denial to its shells.
|
|
71
|
+
`test/server-route-contracts.test.ts` pins the ordering and the exact refusal
|
|
72
|
+
bodies; it exists because the ordering is invisible in any one file and a
|
|
73
|
+
reordering reads like a harmless refactor.
|
|
88
74
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
and the exact refusal bodies; it exists because the ordering is invisible in
|
|
92
|
-
any one file and a reordering reads like a harmless refactor.
|
|
75
|
+
An admitted non-preflight `/mcp` request then takes five steps in
|
|
76
|
+
`src/routes/mcp.ts`:
|
|
93
77
|
|
|
94
|
-
|
|
95
|
-
costs
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
An admitted non-preflight request then takes six steps:
|
|
100
|
-
|
|
101
|
-
1. **Admit.** One permit from the deployment-wide FIFO pool, taken before auth
|
|
102
|
-
so an unauthenticated flood costs a permit rather than a Clerk lookup
|
|
103
|
-
([request admission](./request-admission.md)). The permit is held until the
|
|
104
|
-
response *body* completes, not until the handler returns.
|
|
78
|
+
1. **Admit.** One permit from the deployment-wide pool, taken before auth so an
|
|
79
|
+
unauthenticated flood costs a permit rather than a Clerk lookup, and held
|
|
80
|
+
until the response *body* completes or the caller leaves, not until the
|
|
81
|
+
handler returns.
|
|
105
82
|
2. **Authorize.** Each `InboundAuth` provider's `authorize` in order, bearer
|
|
106
|
-
before interactive
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
3. **
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
6. **Serve.** A fresh `McpServer` per request, the seven meta-tools registered
|
|
123
|
-
against the registry and the response
|
|
124
|
-
handed back.
|
|
83
|
+
before interactive. First `ok` admits; if all fail, the last provider's
|
|
84
|
+
challenge is returned. No providers means open — development only, and it
|
|
85
|
+
warns at construction.
|
|
86
|
+
3. **Narrow to the pool.** On `/mcp/<pool>`, look the name up, run its grant
|
|
87
|
+
against the identity, then `intersectAccess` the pool with the identity's own
|
|
88
|
+
access. A pool can never widen a view; anything else is a 404 naming no pool.
|
|
89
|
+
4. **Derive the registry view.** One `registry.scoped(...)` call with those
|
|
90
|
+
connector ids, exact `connector.tool` addresses when the identity declares a
|
|
91
|
+
narrower slice, and the subject and principal keys. Personal connectors use
|
|
92
|
+
the principal partition, result paging the subject partition, and no caller
|
|
93
|
+
parameter selects either (`test/identity-scope.test.ts`).
|
|
94
|
+
5. **Serve.** Refuse `?toolkit=` with a 404 — the toolkits are gone
|
|
95
|
+
([#178](https://github.com/zackbart/connecta/issues/178)) but their URLs were
|
|
96
|
+
handed out, and retiring a scoping boundary into fail-open is worse than any
|
|
97
|
+
404 — then register the seven meta-tools on a fresh `McpServer`
|
|
98
|
+
(`test/server.test.ts`, `test/code-first-surface.test.ts`).
|
|
125
99
|
|
|
126
100
|
## Layers below the meta-tools
|
|
127
101
|
|
|
128
|
-
The meta-tool handlers are thin. The work sits in
|
|
129
|
-
|
|
102
|
+
The meta-tool handlers are thin. The work sits in five modules the registry owns
|
|
103
|
+
or hands out, and a change usually belongs in exactly one of them:
|
|
130
104
|
|
|
131
105
|
| Module | Owns |
|
|
132
106
|
| --- | --- |
|
|
133
107
|
| `src/registry.ts` | The connector set, identity-scoped views, personal storage partitions, address resolution, catalog TTL/persistence/completeness, refresh single-flight, connector health, per-connector call limiters, and drift. Construction-time refusals live here. |
|
|
134
|
-
| `src/catalog-service.ts` | Request-local
|
|
108
|
+
| `src/catalog-service.ts` | Request-local listing, search, and describe. Caches catalogs inside one request, fans discovery probes out under deadlines, and opts agent reads into the runtime's deferred catalog channel when one exists. |
|
|
135
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. |
|
|
136
|
-
| `src/catalog.ts` | Ranking, description summarizing, and the compact schema
|
|
110
|
+
| `src/catalog.ts` | Ranking, description summarizing, and the compact and TypeScript schema renderers discovery shows. |
|
|
137
111
|
| `src/result-shapes.ts` | Bounded runtime-only inference and merging for output shapes learned from successful read-only calls whose providers declared none. |
|
|
138
112
|
|
|
139
|
-
`src/meta-tools.ts` and `src/execute.ts` are two front doors onto the same
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
the
|
|
145
|
-
would mint a capability.
|
|
113
|
+
`src/meta-tools.ts` and `src/execute.ts` are two front doors onto the same two
|
|
114
|
+
services, `CatalogService` and `InvocationService`. That is the point: a
|
|
115
|
+
program's `connecta.call` and a top-level `call_tool` reach
|
|
116
|
+
`InvocationService.invoke` by different routes and get the same admission, the
|
|
117
|
+
same credential resolution, and the same fail-closed read-only check.
|
|
118
|
+
`test/execute.test.ts` asserts the parity directly, because a sandbox path that
|
|
119
|
+
quietly diverges is how generated code would mint a capability.
|
|
120
|
+
|
|
121
|
+
## Admission, in two places
|
|
122
|
+
|
|
123
|
+
Request admission (`src/executor-admission.ts`, applied in `src/routes/mcp.ts`)
|
|
124
|
+
bounds the MCP envelope: one deployment-wide FIFO pool, plus a deliberately
|
|
125
|
+
smaller code pool a program takes a *second* permit from, so one request cannot
|
|
126
|
+
trade ordinary capacity for unbounded sandboxes. `admission.code` is only a
|
|
127
|
+
fallback — an executor implementing `acquire()` owns a bounded pool already, its
|
|
128
|
+
settings win, and connecta warns the fallback was ignored. Invalid bounds throw
|
|
129
|
+
at construction, because a pool that quietly became unbounded is worse than a
|
|
130
|
+
deployment that refuses to boot. The queue is global FIFO across identities: a
|
|
131
|
+
capacity boundary, not tenant fairness, and one deployment serves one tenant.
|
|
132
|
+
|
|
133
|
+
Call admission (`src/call-admission.ts`) answers what the envelope cannot see —
|
|
134
|
+
a connector's optional policy over its own `Connector.callTool` attempts,
|
|
135
|
+
partitioned by an optional `partitionKey` and bounded by concurrency, a
|
|
136
|
+
rolling-window budget, or both. Exactly one rule is accepted, because several
|
|
137
|
+
cannot be faked as sequential leases: consuming a rolling token before a later
|
|
138
|
+
rule refuses would charge a call that never reached the provider, the exact
|
|
139
|
+
accounting error a budget exists to prevent. Both layers are pinned by
|
|
140
|
+
`test/request-admission.test.ts` and `test/call-admission.test.ts`.
|
|
141
|
+
|
|
142
|
+
## Storage, credentials, and connectors
|
|
143
|
+
|
|
144
|
+
`KVStorage` is `get`/`set`/`delete` with optional `list(prefix)`; core uses it for
|
|
145
|
+
connector state, catalogs, and result paging — a 15-minute TTL with one
|
|
146
|
+
runtime-wide accounting of stash bytes and entries, where a full stash returns the
|
|
147
|
+
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 atomic
|
|
149
|
+
claim: `null` means absent (expired counts) on the way in and delete on the way
|
|
150
|
+
out. Nothing in core requires it yet. Downstream OAuth uses it where the store
|
|
151
|
+
has it, so resealing legacy plaintext and discarding a refused grant cannot
|
|
152
|
+
overwrite a consent that landed in between, and falls back to a read and a
|
|
153
|
+
write where it does not. A subsystem that needs an exactly-once claim will
|
|
154
|
+
require it explicitly rather than emulate it that way.
|
|
155
|
+
Adapters: `src/storage/memory.ts` and `src/storage/file.ts` (Node) both provide
|
|
156
|
+
it, and the namespaced views core hands connectors forward it only when the
|
|
157
|
+
underlying store has it. `examples/worker/` carries two more: Cloudflare KV,
|
|
158
|
+
eventually consistent and so declaring none, and a D1 store that provides it —
|
|
159
|
+
copyable reference source beside the D1 activity store, deliberately not an
|
|
160
|
+
importable subpath. The shared cases live in `test/storage-contract.ts`.
|
|
161
|
+
|
|
162
|
+
`src/credentials.ts` is the AES-GCM vault behind the root-exported
|
|
163
|
+
`CredentialVault` contract, selected through the `vault` slot. It binds connector
|
|
164
|
+
id and owner into the authenticated encryption context, because sharing a backend
|
|
165
|
+
is not permission to share a principal's credentials. Two rules carry the
|
|
166
|
+
subsystem: credentials never leave the host — read only through the owning
|
|
167
|
+
connector's `ctx.credential`, rendered by nothing, absent from activity and model
|
|
168
|
+
recovery — and they fail at use, proactive liveness probing having been removed by
|
|
169
|
+
decision. The vault is read per call, so a replacement needs no restart
|
|
170
|
+
(`test/credentials.test.ts`).
|
|
171
|
+
|
|
172
|
+
Connectors are the boundary between the fixed meta-tool surface and downstream
|
|
173
|
+
capability, and `api()`, `remoteMcp()`, and a hand-written `Connector`
|
|
174
|
+
(`src/connectors/`, plus the prebuilt connections under `src/providers/`) all
|
|
175
|
+
produce instances that take the same catalog, read-only, credential, storage,
|
|
176
|
+
invocation, result-size, and activity paths. Every one is deployment
|
|
177
|
+
configuration, never runtime registration. `authScope: "shared" | "personal"`
|
|
178
|
+
partitions connecta-owned context — state, credentials, OAuth, catalogs, observed
|
|
179
|
+
shapes — by principal, and *only* connecta-owned context: a secret a custom
|
|
180
|
+
handler closes over is shared JavaScript state, and `remoteMcp()` refuses the
|
|
181
|
+
literal-headers-plus-personal version of that mistake. Visibility
|
|
182
|
+
(`identity.connectorAccess`) is a separate rule; hiding a connector does not
|
|
183
|
+
change who owns its auth.
|
|
146
184
|
|
|
147
185
|
## Optional deployment modules
|
|
148
186
|
|
|
149
|
-
`createConnecta` takes closed typed `ui`, `vault`, and `activity` slots
|
|
150
|
-
|
|
151
|
-
`/auth/bearer`. Root exports the contracts, never
|
|
152
|
-
is no module array, runtime registration, or plugin lifecycle.
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
187
|
+
`createConnecta` takes closed typed `ui`, `vault`, and `activity` slots, with
|
|
188
|
+
factories at `/ui`, `/credentials`, and `/activity` and bearer auth at
|
|
189
|
+
`/auth/bearer`. Root exports the contracts, never the implementations, and there
|
|
190
|
+
is no module array, runtime registration, or plugin lifecycle. Core keeps
|
|
191
|
+
discovery, the executor contract, invocation, permissions, and OAuth callback
|
|
192
|
+
verification; an omitted module contributes no runtime work at all.
|
|
193
|
+
|
|
194
|
+
The operator UI — `src/ui.ts` (data-free shell and `/ui/data` payload),
|
|
195
|
+
`src/routes/ui.ts`, `src/operator-ui/` (the Preact app and its pure rules) —
|
|
196
|
+
shows a human what a deployment exposes and manages only the authentication
|
|
197
|
+
material code explicitly permitted; it never edits the connector set, catalog,
|
|
198
|
+
annotations, scopes, or permission rules. Two invariants shape it: a status read
|
|
199
|
+
never starts authorization, since OAuth begins with an explicit authorized POST,
|
|
200
|
+
and each lazy details request owns a bounded downstream scope, so one failing
|
|
201
|
+
provider leaves the other connections usable. A connector's status message
|
|
202
|
+
never reaches the page: it can quote a downstream error body, and that body can
|
|
203
|
+
quote the secret it rejected, so the details payload carries only a classified
|
|
204
|
+
`problem` — which picks fixed on-screen copy and a fixed fix prompt — and the raw
|
|
205
|
+
text goes to the server log. Credential handoff URLs exist only while the UI is
|
|
206
|
+
mounted; OAuth callbacks never need it.
|
|
207
|
+
|
|
208
|
+
Its appearance is one token layer. `src/operator-ui/browser.css` resolves every
|
|
209
|
+
color, radius, and font through a custom property and mixes the rest from those
|
|
210
|
+
with `color-mix`, so `branding.theme` only has to append a `:root` block after
|
|
211
|
+
that stylesheet. The five tokens it accepts are gated in `src/branding.ts`, each
|
|
212
|
+
by a narrow syntactic check: deployment config reaches a `<style>` element here,
|
|
213
|
+
and an unvalidated value would be CSS injection. The dark palette is the same
|
|
214
|
+
tokens under `prefers-color-scheme`; `colorScheme` pins one with a `data-scheme`
|
|
215
|
+
attribute on the page.
|
|
159
216
|
|
|
160
217
|
## Import-graph purity
|
|
161
218
|
|
|
162
|
-
Nothing reachable from `src/index.ts` may import a `node:` builtin
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
219
|
+
Nothing reachable from `src/index.ts` may import a `node:` builtin, so the same
|
|
220
|
+
core runs unchanged in workerd and in Node. The Node-touching paths — `src/node.ts`
|
|
221
|
+
(the `node:http` adapter), `src/storage/file.ts`, and the QuickJS process pool
|
|
222
|
+
(`src/executors/quickjs.ts` plus its child) — each sit behind an explicit subpath
|
|
223
|
+
and must stay unreachable from the root. `./auth/clerk` is separate because
|
|
224
|
+
`@clerk/backend` is an optional peer rather than a dependency, and
|
|
225
|
+
`./auth/cloudflare-access` for a third reason: it is Web-API-pure, but its trust
|
|
226
|
+
contract is specific to a direct Worker invocation carrying `ctx.access`.
|
|
227
|
+
|
|
228
|
+
`test/purity.test.ts` walks the relative-import graph and fails on any `node:`
|
|
229
|
+
specifier in a reachable file, or on any of those modules — plus the UI bundle,
|
|
230
|
+
encrypted vault, and activity implementation — being reachable at all;
|
|
231
|
+
`test/package-surface.test.ts` and `scripts/check-package.mjs` guard the same
|
|
232
|
+
boundary in the published tarball. The failure mode is not theoretical: one
|
|
233
|
+
convenience import of `node:crypto` in a shared helper stops the whole Worker
|
|
234
|
+
shape from building, in someone else's repository rather than this one.
|
|
235
|
+
|
|
236
|
+
## Effect inside
|
|
237
|
+
|
|
238
|
+
The core runs on Effect v4; no API a deployment touches does. `createConnecta`,
|
|
239
|
+
`remoteMcp()`, `api()`, the `Connector` contract, and every shipped `.d.ts`
|
|
240
|
+
are Promise-shaped and name no Effect type, so a connector author never meets a
|
|
241
|
+
second async paradigm. The reason for the rewrite is the first section of this
|
|
242
|
+
guide: the bugs here are lifetime bugs, and Effect makes a lifetime a value —
|
|
243
|
+
a Scope that closes on every exit, an interrupt that reaches whatever a fiber
|
|
244
|
+
is waiting on, a Deferred that one party completes and any number join. The
|
|
245
|
+
conversion found and closed a dozen of them, each with a test that fails on the
|
|
246
|
+
code before it: permits and leases held after their caller left, waits with no
|
|
247
|
+
bound, and races between requests.
|
|
248
|
+
|
|
249
|
+
### Shell and core
|
|
250
|
+
|
|
251
|
+
Each converted module is a shell and a core. The shell keeps its exported class
|
|
252
|
+
or function exactly — same members, same error classes, same `.d.ts`, private
|
|
253
|
+
member names included, because TypeScript ships those too. The core is an
|
|
254
|
+
Effect program behind it. In a published class it lives in a `static` block,
|
|
255
|
+
the one place that can read private state without adding a member to the
|
|
256
|
+
declarations; elsewhere it lives in an unexported function or in
|
|
257
|
+
`src/runtime/`, which no `exports` target reaches. Errors keep their classes
|
|
258
|
+
(`ConnectorCallError`, `ExecutorAdmissionError`, `CallAdmissionError`) because
|
|
259
|
+
callers and tests check `instanceof`, `name`, and `message`.
|
|
260
|
+
|
|
261
|
+
Effect callers skip the Promise and take the program: `admit` and
|
|
262
|
+
`acquireScoped` for request admission (`src/runtime/admission.ts`), `admitCall`
|
|
263
|
+
and `acquireCallScoped` for call admission, `closeScope` and `closeScopeOnExit`
|
|
264
|
+
for a connector scope, `withDeadlineEffect` where Promise code has
|
|
265
|
+
`withDeadline`. The build prunes every declaration no `exports` target reaches,
|
|
266
|
+
and `npm run check:declarations` fails if what remains names an Effect type.
|
|
267
|
+
|
|
268
|
+
### One runner
|
|
269
|
+
|
|
270
|
+
`src/runtime/run.ts` is the only place a fiber starts, and
|
|
271
|
+
`test/purity.test.ts` fails if any other file calls `Effect.run*`, `runFork`,
|
|
272
|
+
`forkDaemon`, or `ManagedRuntime.make`. It exports two ways out:
|
|
273
|
+
|
|
274
|
+
- **`runEdge(effect, { signal, runtime? })`** runs an effect at a Promise
|
|
275
|
+
boundary. It rethrows the original failure or defect, never a wrapper. It
|
|
276
|
+
turns an interrupt into the caller's `signal.reason` rather than Effect's
|
|
277
|
+
"All fibers interrupted without error". And it runs every fiber on a
|
|
278
|
+
scheduler that yields through microtasks: Effect's default yields through
|
|
279
|
+
`setImmediate`, which vitest's fake timers freeze and Workers never had. The
|
|
280
|
+
price is that a fiber never yields to I/O, so CPU-heavy work stays out of
|
|
281
|
+
Effect loops.
|
|
282
|
+
- **`detach(effect, ctx)`** is the only fire-and-forget, and it hands the work
|
|
283
|
+
to `ctx.waitUntil` when there is one.
|
|
284
|
+
|
|
285
|
+
Beside them sit `withDeadlineEffect`, which aborts the operation's own signal
|
|
286
|
+
with the labelled timeout error *before* interrupting it, so work that honors
|
|
287
|
+
the signal sees the same reason the caller does, and `fromSignal`, which turns
|
|
288
|
+
an `AbortSignal` into a failure to race against. Nothing runs Effect at module
|
|
289
|
+
scope, where a Worker may not start work, and nothing logs through
|
|
290
|
+
`Effect.log`: logging goes through the configured `Logger`, which is what
|
|
291
|
+
honors `logger: "silent"`.
|
|
292
|
+
|
|
293
|
+
Each Connecta also gets a runtime of its own (`src/runtime/services.ts`):
|
|
294
|
+
`Storage`, `Vault`, `ActivityRecorder` (one that records nothing when the module
|
|
295
|
+
is omitted), `Logger`, and `ResolvedConfig`. Creating it builds nothing, since
|
|
296
|
+
a Worker constructs its Connecta at global scope; the first run that needs the
|
|
297
|
+
services builds them, and `close()` disposes the runtime last. Two things
|
|
298
|
+
deliberately do not run on it. The request pipeline runs on no runtime, so
|
|
299
|
+
`/health` and a closed deployment's 503 keep answering after `close()`. And a
|
|
300
|
+
registry provides its own storage and logger to its programs
|
|
301
|
+
(`runOnPartition` in `src/runtime/storage.ts`), because a personal registry's
|
|
302
|
+
storage is the root's namespaced to its principal and the runtime's `Storage`
|
|
303
|
+
would be another partition's.
|
|
304
|
+
|
|
305
|
+
### What runs on Effect
|
|
306
|
+
|
|
307
|
+
| Area | Shape |
|
|
308
|
+
| --- | --- |
|
|
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. |
|
|
310
|
+
| 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
|
+
| 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
|
+
| `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
|
+
| Discovery (`catalog-service.ts`) | A request-scoped cache: one Deferred per connector, completed by the first asker's read from that read's own settlement, so a probe deadline ends one asker's wait and never the read the others share. Fan-out is `Effect.forEach` under the discovery concurrency. |
|
|
314
|
+
| Registry (`registry.ts`) | Catalog persistence and the result stash are programs over `Storage`. A refresh flight is a Deferred its publishing request completes; persisted-catalog writes take per-connector turns, each a Deferred its own request completes. |
|
|
315
|
+
| 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
|
+
| 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
|
+
| 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. |
|
|
318
|
+
| QuickJS pool (`executors/quickjs.ts`, Node only) | Each child is a scoped resource whose release sends SIGTERM, then SIGKILL after a second; crash respawn backoff is a `Schedule`. |
|
|
319
|
+
|
|
320
|
+
### What stays plain
|
|
321
|
+
|
|
322
|
+
- **Everything a deployment writes against.** The `Connector` contract,
|
|
323
|
+
`api()`, the providers, custom connectors, storage adapters, and the vault
|
|
324
|
+
are Promises. A connector is called with `Effect.tryPromise`, never asked to
|
|
325
|
+
return an Effect.
|
|
326
|
+
- **Inbound authorization.** `authorize` in `src/routes/shared.ts` ships in
|
|
327
|
+
the declarations and in the `./activity` bundle; converting it cost 32 KB
|
|
328
|
+
gzip there for nothing a caller could see.
|
|
329
|
+
- **Pure synchronous code.** Ranking, schema rendering, classification,
|
|
330
|
+
`validateToolInput`, the host-call budget, the emit collector, failure
|
|
331
|
+
framing, and result shaping. The microtask scheduler never yields to I/O, so
|
|
332
|
+
wrapping them would buy nothing and could starve a request.
|
|
333
|
+
- **The MCP edges.** `@modelcontextprotocol/server` upward and the SDK client
|
|
334
|
+
downstream; Effect's own `McpServer` is not used.
|
|
335
|
+
- **The Node and browser leaves.** `node.ts`, `fileStorage`, the QuickJS
|
|
336
|
+
child, runtime, and protocol, and the operator UI's browser app.
|
|
337
|
+
|
|
338
|
+
### Across requests
|
|
339
|
+
|
|
340
|
+
Work shared across requests meets only through a Deferred that the owning
|
|
341
|
+
request completes, never through a fiber that outlives its request. Two
|
|
342
|
+
workerd rules follow, and both have bitten.
|
|
343
|
+
|
|
344
|
+
**Waiting on a shared Deferred is its own edge.** Deferred resumes a waiting
|
|
345
|
+
fiber synchronously, inside the call that completed it, which on Workers means
|
|
346
|
+
inside the completing request's I/O context. A waiter that goes on to touch its
|
|
347
|
+
own body, transport, or storage there fails with "Cannot perform I/O on behalf
|
|
348
|
+
of a different request", or hangs until its deadline. So the wait runs through
|
|
349
|
+
`runEdge`, and the waiter does its own I/O after awaiting that promise, which
|
|
350
|
+
workerd resumes in the waiter's request as it does any promise resolved from
|
|
351
|
+
another. The same holds one level out for every queue: a queued call, a queued
|
|
352
|
+
`execute_code`, and a queued `/mcp` request are each handed their permit from
|
|
353
|
+
inside the request that released one, so each awaits the controller's
|
|
354
|
+
Promise-shaped `acquire` rather than yielding its Effect program, and does its
|
|
355
|
+
own work only once that promise resolves. Effect's `Semaphore` is not used
|
|
356
|
+
across requests for the same reason, and because a same-tick newcomer can take
|
|
357
|
+
a released permit ahead of a waiter that queued earlier.
|
|
358
|
+
|
|
359
|
+
**Never read another request's signal.** On workerd, reading `aborted` or
|
|
360
|
+
`reason`, or calling `abort()`, on a signal that belongs to another request
|
|
361
|
+
throws; adding and removing listeners does not. Code that runs from another
|
|
362
|
+
request's completion — a queue handing out a permit, `close()` — must not look
|
|
363
|
+
at a waiter's signal. A waiter whose signal aborts leaves the queue from its
|
|
364
|
+
own listener, in its own request.
|
|
365
|
+
|
|
366
|
+
One corollary is easy to break by tidying. Workerd cancels a request as hung
|
|
367
|
+
when its only pending work is waiting on another request and it has no timer or
|
|
368
|
+
I/O of its own. Connecta's deadline timers are what keep a waiting request
|
|
369
|
+
alive and bounded, so no wait may lose its timer.
|
|
370
|
+
|
|
371
|
+
### Why not Schema or HttpApi
|
|
372
|
+
|
|
373
|
+
Both were measured and both lost. `effect` is imported through its barrel,
|
|
374
|
+
which defeats tree-shaking: the first conversion step measured about +20 ms
|
|
375
|
+
of Worker cold start for it, and that cost was accepted over deep imports at
|
|
376
|
+
every call site. The catch is that a module the barrel reaches is paid for in
|
|
377
|
+
full, however little of it is used. Effect Schema for config validation and the
|
|
378
|
+
meta-tool inputs came to about +62 KB gzip and +13 ms of cold start in the
|
|
379
|
+
root, for a validator no clearer than the hand-written `CONFIG_SCHEMA`; the
|
|
380
|
+
meta-tool inputs stay zod, which the MCP SDK bundles regardless. `HttpApi` for
|
|
381
|
+
the operator and activity routes cost about +100 KB gzip on `./ui` and +130 KB
|
|
382
|
+
on `./activity`, and matching the wire format meant opting out of most of what
|
|
383
|
+
it does: unowned paths fall through rather than 404, a wrong method is a JSON
|
|
384
|
+
405, and the content-type and size checks run before a body is read. The root
|
|
385
|
+
entry may import only `effect` itself (`test/purity.test.ts`); nothing in
|
|
386
|
+
`src/` uses `effect/unstable/*`, which would have to stay behind a subpath.
|
|
387
|
+
|
|
388
|
+
The core's cost is recorded rather than guessed. Against 0.24.4 the root entry
|
|
389
|
+
grew from 235,346 to 279,526 bytes gzip, the Worker example from 263,949 to
|
|
390
|
+
314,336, and each provider by about 36 KB, since each reaches Effect through
|
|
391
|
+
`remoteMcp()`; `npm run check:bundle` caps every entry against
|
|
392
|
+
`scripts/bundle-budget.json`. An install carries about 53 MB of `effect`, with
|
|
393
|
+
no dependencies of its own.
|
|
394
|
+
|
|
395
|
+
## Where else to look
|
|
396
|
+
|
|
397
|
+
Beyond the modules already named:
|
|
188
398
|
|
|
189
399
|
```
|
|
190
400
|
src/
|
|
191
|
-
index.ts createConnecta + the public re-exports (Workers-clean)
|
|
192
401
|
server.ts route ordering, HTTPS upgrade, security wrapper
|
|
193
402
|
routes/ one file per surface; shared.ts holds the auth gate
|
|
194
|
-
meta-tools.ts the six non-execute meta-tools over the registry
|
|
195
|
-
execute.ts execute_code, the sandbox host bridge, emitted media
|
|
196
403
|
skills.ts MCP instructions, the usage skill, connector guides
|
|
197
|
-
registry.ts connector set, addresses, health, call limiters
|
|
198
|
-
catalog-service.ts request-local catalog access, search, and describe
|
|
199
|
-
catalog.ts ranking, summaries, compact schema rendering
|
|
200
|
-
result-shapes.ts passive runtime-only observed output schemas
|
|
201
|
-
invocation.ts one tool call, end to end
|
|
202
404
|
catalog-drift.ts vetted manifests and the counts a refresh produces
|
|
203
|
-
credentials.ts the AES-GCM connector vault over KVStorage
|
|
204
405
|
activity.ts optional history factory and best-effort recorder
|
|
205
|
-
call-admission.ts connector-partitioned downstream permits and budgets
|
|
206
|
-
executor-admission.ts the portable bounded queue both pools use
|
|
207
|
-
ui.ts the served operator shell and /ui/data payload
|
|
208
|
-
operator-ui/ the Preact app, its pure rules, and the built bundle
|
|
209
|
-
connectors/ remote-mcp.ts, api.ts, guarded-fetch.ts
|
|
210
|
-
providers/ the maintained prebuilt connections
|
|
211
406
|
auth/ bearer, Cloudflare Access, clerk (optional peer), downstream OAuth
|
|
212
407
|
executors/ the QuickJS pool and child (Node only)
|
|
213
|
-
storage/ memory.ts, file.ts (Node only)
|
|
214
408
|
node.ts listen() + fileStorage re-export (Node only)
|
|
215
409
|
```
|
|
216
410
|
|
|
411
|
+
There are exactly two deployment shapes — `templates/node/`, which
|
|
412
|
+
`connecta init` copies with its container files, and `examples/worker/` — and
|
|
413
|
+
`test/deployment-shapes.test.ts` with `npm run check:examples` keeps both
|
|
414
|
+
compiling and configuring the real thing.
|
|
415
|
+
|
|
217
416
|
## Sharp edges
|
|
218
417
|
|
|
219
418
|
- **The root registry is shared; identity views are partitioned.** Shared
|
|
220
|
-
connector caches are visible to later requests in the isolate
|
|
221
|
-
connectors use a bounded principal registry
|
|
222
|
-
authenticated subject.
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
-
|
|
228
|
-
|
|
229
|
-
- **`close()` is idempotent and ordered.** It closes both admission pools and
|
|
230
|
-
the connector limiters, then the executor. Node's `listen()` calls it on
|
|
419
|
+
connector caches are visible to later requests in the isolate; personal
|
|
420
|
+
connectors use a bounded principal registry and transient results the
|
|
421
|
+
authenticated subject. Putting a downstream client or credential on the wrong
|
|
422
|
+
side of those lines is the highest-severity mistake available here.
|
|
423
|
+
- **Route order is behavior.** Moving a mutation route below the wildcard
|
|
424
|
+
`OPTIONS` opts it into CORS preflight; reordering admission after auth makes
|
|
425
|
+
the cheapest possible attack the most expensive request.
|
|
426
|
+
- **`close()` is idempotent and ordered.** Both admission pools, then the
|
|
427
|
+
connector limiters, then the executor, then the Connecta's runtime; Node's `listen()` calls it on
|
|
231
428
|
SIGTERM/SIGINT.
|
|
232
429
|
- **Structural mistakes throw at construction.** A duplicate connector id, an
|
|
233
|
-
invalid admission rule, removed `accessTokens` option,
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
## Tests that enforce this
|
|
237
|
-
|
|
238
|
-
| Invariant | Suite |
|
|
239
|
-
| --- | --- |
|
|
240
|
-
| The core imports no `node:` builtin and reaches no Node-only module | `test/purity.test.ts` |
|
|
241
|
-
| The published surface matches the same boundary | `test/package-surface.test.ts`, `scripts/check-package.mjs` |
|
|
242
|
-
| Route order, per-route auth, and byte-exact refusals | `test/server-route-contracts.test.ts` |
|
|
243
|
-
| `/mcp` end to end, the open routes, exactly seven tools, bounded connector orientation | `test/server.test.ts`, `test/code-first-surface.test.ts` |
|
|
244
|
-
| Construction-time refusals and the grouped config boundary | `test/config.test.ts`, `test/registry.test.ts` |
|
|
245
|
-
| Program and top-level calls take the same enforced path | `test/execute.test.ts` |
|
|
246
|
-
| Both deployment shapes still compile and configure the real thing | `test/deployment-shapes.test.ts`, `npm run check:examples` |
|
|
430
|
+
invalid admission rule, the removed `accessTokens` option, a missing executor:
|
|
431
|
+
all refuse to boot (`test/config.test.ts`, `test/registry.test.ts`). Starting
|
|
432
|
+
in the wrong shape is worse than not starting.
|