@zackbart/connecta 0.24.3 → 0.24.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/AGENTS.md +18 -20
  2. package/CHANGELOG.md +64 -1
  3. package/README.md +5 -6
  4. package/dist/branding.d.ts +31 -2
  5. package/dist/branding.js +116 -8
  6. package/dist/connectors/api.d.ts +1 -1
  7. package/dist/connectors/api.js +10 -2
  8. package/dist/connectors/guarded-fetch.d.ts +5 -1
  9. package/dist/connectors/guarded-fetch.js +34 -4
  10. package/dist/connectors/remote-mcp.js +8 -4
  11. package/dist/errors.d.ts +11 -3
  12. package/dist/errors.js +2 -1
  13. package/dist/index.d.ts +1 -1
  14. package/dist/index.js +12 -1
  15. package/dist/meta-tools.js +105 -29
  16. package/dist/operator-ui/generated.js +2 -2
  17. package/dist/operator-ui/view.d.ts +38 -1
  18. package/dist/operator-ui/view.js +71 -0
  19. package/dist/providers/cloudflare.d.ts +14 -2
  20. package/dist/providers/cloudflare.js +107 -16
  21. package/dist/providers/linear.d.ts +26 -4
  22. package/dist/providers/linear.js +19 -4
  23. package/dist/providers/mixpanel.d.ts +16 -3
  24. package/dist/providers/mixpanel.js +13 -2
  25. package/dist/providers/notion.d.ts +8 -1
  26. package/dist/providers/notion.js +83 -10
  27. package/dist/providers/revenuecat.d.ts +30 -4
  28. package/dist/providers/revenuecat.js +42 -4
  29. package/dist/providers/stripe.d.ts +7 -1
  30. package/dist/providers/stripe.js +30 -4
  31. package/dist/providers/vercel.js +11 -1
  32. package/dist/registry.d.ts +12 -4
  33. package/dist/registry.js +22 -8
  34. package/dist/types.d.ts +37 -0
  35. package/dist/ui.js +18 -10
  36. package/dist/version.d.ts +1 -1
  37. package/dist/version.js +1 -1
  38. package/documentation/architecture.md +193 -181
  39. package/documentation/auth.md +197 -176
  40. package/documentation/code-mode.md +426 -321
  41. package/documentation/meta-tools.md +356 -416
  42. package/examples/worker/AGENTS.md +2 -1
  43. package/examples/worker/README.md +12 -10
  44. package/examples/worker/src/index.ts +12 -15
  45. package/package.json +1 -2
  46. package/templates/node/.env.example +3 -3
  47. package/templates/node/AGENTS.md +5 -4
  48. package/templates/node/README.md +2 -1
  49. package/templates/node/package.json +1 -1
  50. package/templates/node/src/index.ts +23 -22
  51. package/documentation/call-admission.md +0 -158
  52. package/documentation/cloudflare.md +0 -471
  53. package/documentation/connector-guides.md +0 -176
  54. package/documentation/connectors.md +0 -431
  55. package/documentation/linear.md +0 -193
  56. package/documentation/mixpanel.md +0 -160
  57. package/documentation/notion.md +0 -308
  58. package/documentation/operations.md +0 -359
  59. package/documentation/operator-ui.md +0 -135
  60. package/documentation/optional-modules-upgrade.md +0 -243
  61. package/documentation/provider-conventions.md +0 -729
  62. package/documentation/request-admission.md +0 -204
  63. package/documentation/revenuecat.md +0 -305
  64. package/documentation/storage-and-credentials.md +0 -254
  65. package/documentation/stripe.md +0 -262
  66. package/documentation/upgrading.md +0 -768
  67. package/documentation/vercel.md +0 -241
@@ -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. This guide says how the shape it
8
- describes is actually assembled, and where a change is likely to break it.
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,244 @@ 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 }`. The `Registry` owns the connector set, address
17
- resolution, catalog caches, observed output schemas, connector health, and the
18
- per-connector call limiters. It is constructed once and lives as long as the
19
- isolate or process
20
- on Workers that means a lazy module-scope singleton, which is why both
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 concurrent request scopes but
25
- never shares their clients, transports, or responses, and never lets a follower
26
- cancel the owner. Every participant still awaits the refresh inside its own
27
- request lifetime; a cancelled follower leaves the shared owner untouched and
28
- removes only its own wait. The owner's request signal belongs to its token
29
- fetch. Cancelling that owner fails current joiners too because promoting one
30
- could replay a refresh token the authorization server already consumed.
31
-
32
- A valid token response is a consumed refresh token whether or not the owner
33
- survives to save it. The coordinator therefore keeps the accepted tokens on the
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
- ethos invariant, not a style preference: a client retained across requests on
53
- Workers is a cross-request capability leak, and a promise awaited after the
54
- response is work the runtime may have already torn down. Deferred work has one
55
- sanctioned channel `ctx.waitUntil`, threaded through `fetch(request, env,
56
- ctx)`. Best-effort activity writes use it. An agent read that already demanded
57
- an expired catalog refresh may also use it while serving a complete catalog
58
- inside its stale window. That refresh owns a new scope and deadline; it never
59
- carries the inbound scope or signal past the request.
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 resumability, scope resolved rather than remembered.
65
47
 
66
48
  ## Request lifecycle
67
49
 
68
- `src/server.ts` is the composition root. It checks MCP origins, upgrades the
69
- scheme when it must, runs the route table, then wraps whatever came back in
70
- security headers. Route *order* is the contractseveral routes would behave
71
- differently if they were reachable in another order — so the table below is
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 | `/mcp` and every `/mcp/` suffix reject a disallowed `Origin` with a fixed 403 before redirects, admission, auth, or preflight. `allowedOrigins` defaults to the configured public origin plus HTTP(S) loopback origins at any port. Requests without Origin are admitted. |
77
- | 0 | HTTPS upgrade | 308 to `publicUrl` when it is HTTPS and the request arrived over HTTP. Path and query are *assigned* onto the configured URL, never resolved against it, so a `//host` pathname cannot replace the deployment origin. `/health` is exempt: a loopback container probe must not depend on public DNS and TLS. `/ui` is canonicalized to `/` while upgrading. |
78
- | 0 | Cloudflare Access (Worker deployment, when enabled) | Edge admission before this route table. Managed OAuth owns its challenge and discovery metadata; an admitted direct invocation carries trusted identity in `ctx.access`. |
79
- | 1 | Mounted UI routes | The optional UI handles its shells, assets, data, details, and auth mutations before wildcard OPTIONS. Mutation routes refuse preflight rather than inheriting MCP CORS. No UI module means none of these routes. |
80
- | 2 | MCP preflight | Allowed `OPTIONS` on `/mcp` or any `/mcp/` suffix returns 204 without admission or auth. Reflect the allowed origin and requested valid `mcp-param-*` header names. |
81
- | 2 | Other `OPTIONS` | Auth metadata gets a chance, otherwise compatibility CORS preflight. |
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; connector drift uses stable short hashes and downstream admission sums shared and personal controllers without ids. Reserved routes reflect installed modules. |
84
- | 5 | `/oauth/callback/<connectorId>` | Core downstream OAuth completion, state verification and personal ownership checks; independent of UI. |
85
- | 6 | `/mcp`, `/mcp/<pool>` | Origin check before admission, admission before auth, then a request-local MCP server. A pool path serves the declared pool intersected with the identity's own view; any undeclared suffix, including malformed names, a refusing grant, and a throwing grant return the same 404 status, body, and headers after auth. Grant lookup latency is not hidden; see [pools](./auth.md#pools). |
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
- Every response leaves through `withSecurityHeaders`: `nosniff`, a no-referrer
90
- policy, HSTS on HTTPS, while the UI module adds a nonce-based script CSP and framing denial to its shells. `test/server-route-contracts.test.ts` pins this ordering
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
- `/mcp` first checks Origin, including on preflight. A disallowed browser origin
95
- costs no permit and no auth lookup. The explicit `allowedOrigins: "*"` escape
96
- hatch preserves open CORS; a list reflects only admitted origins and varies
97
- responses by Origin. An originless client needs no CORS allow-origin header.
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, not until the handler returns.
105
81
  2. **Authorize.** Each `InboundAuth` provider's `authorize` in order, bearer
106
- before interactive providers. First `ok` admits; if all fail, the last provider's challenge
107
- response is returned. No providers configured means open — development
108
- only, and it warns at construction.
109
- 3. **Derive the registry view.** Auth supplies a namespaced subject and, for a
110
- human, a principal. `identity.connectorAccess` selects declared connector
111
- ids and, for a narrower slice, exact `connector.tool` addresses; the
112
- scoped view filters every catalog read through them. Personal connectors use the principal partition; result paging uses
113
- the subject partition. No caller parameter selects either.
114
- 4. **Narrow to the pool.** On `/mcp/<pool>`, look the name up in the
115
- declared pools and run its grant against the authenticated identity. The
116
- view becomes the pool intersected with the identity's `connectorAccess`;
117
- a pool can never widen it. Anything else is a 404 that names no pool.
118
- 5. **Refuse `?toolkit=`.** Caller-selected toolkits were removed ([#178](https://github.com/zackbart/connecta/issues/178))
119
- but the URLs naming them were handed out, so the parameter is a 404 rather
120
- than silently serving the full registry. Retiring a scoping boundary into
121
- fail-open is the one outcome worse than the 404.
122
- 6. **Serve.** A fresh `McpServer` per request, the seven meta-tools registered
123
- against the registry and the response
124
- handed back.
82
+ before interactive. First `ok` admits; if all fail, the last provider's
83
+ challenge is returned. No providers means open — development only, and it
84
+ warns at construction.
85
+ 3. **Narrow to the pool.** On `/mcp/<pool>`, look the name up, run its grant
86
+ against the identity, then `intersectAccess` the pool with the identity's own
87
+ access. A pool can never widen a view; anything else is a 404 naming no pool.
88
+ 4. **Derive the registry view.** One `registry.scoped(...)` call with those
89
+ connector ids, exact `connector.tool` addresses when the identity declares a
90
+ narrower slice, and the subject and principal keys. Personal connectors use
91
+ the principal partition, result paging the subject partition, and no caller
92
+ parameter selects either (`test/identity-scope.test.ts`).
93
+ 5. **Serve.** Refuse `?toolkit=` with a 404 the toolkits are gone
94
+ ([#178](https://github.com/zackbart/connecta/issues/178)) but their URLs were
95
+ handed out, and retiring a scoping boundary into fail-open is worse than any
96
+ 404 then register the seven meta-tools on a fresh `McpServer`
97
+ (`test/server.test.ts`, `test/code-first-surface.test.ts`).
125
98
 
126
99
  ## Layers below the meta-tools
127
100
 
128
- The meta-tool handlers are thin. The work sits in four services the registry
129
- owns or hands out, and a change usually belongs in exactly one of them:
101
+ The meta-tool handlers are thin. The work sits in five modules the registry owns
102
+ or hands out, and a change usually belongs in exactly one of them:
130
103
 
131
104
  | Module | Owns |
132
105
  | --- | --- |
133
106
  | `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 tool listing, search, and describe. It coalesces reads inside one request and opts agent reads into the runtime's deferred catalog channel when one exists. |
107
+ | `src/catalog-service.ts` | Request-local listing, search, and describe. Coalesces reads inside one request and opts agent reads into the runtime's deferred catalog channel when one exists. |
135
108
  | `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
109
  | `src/catalog.ts` | Ranking, description summarizing, and the compact schema renderer discovery shows. |
137
110
  | `src/result-shapes.ts` | Bounded runtime-only inference and merging for output shapes learned from successful read-only calls whose providers declared none. |
138
111
 
139
- `src/meta-tools.ts` and `src/execute.ts` are two front doors onto the same
140
- three services. That is the point: a program's `connecta.call` and a top-level
141
- `call_tool` reach `InvocationService.invoke` by different routes and get the
142
- same admission, the same credential resolution, and the same fail-closed
143
- read-only check. `test/execute.test.ts` asserts that parity directly, because
144
- the alternative a sandbox path that quietly diverges — is how generated code
145
- would mint a capability.
112
+ `src/meta-tools.ts` and `src/execute.ts` are two front doors onto the same two
113
+ services, `CatalogService` and `InvocationService`. That is the point: a
114
+ program's `connecta.call` and a top-level `call_tool` reach
115
+ `InvocationService.invoke` by different routes and get the same admission, the
116
+ same credential resolution, and the same fail-closed read-only check.
117
+ `test/execute.test.ts` asserts the parity directly, because a sandbox path that
118
+ quietly diverges is how generated code would mint a capability.
119
+
120
+ ## Admission, in two places
121
+
122
+ Request admission (`src/executor-admission.ts`, applied in `src/routes/mcp.ts`)
123
+ bounds the MCP envelope: one deployment-wide FIFO pool, plus a deliberately
124
+ smaller code pool a program takes a *second* permit from, so one request cannot
125
+ trade ordinary capacity for unbounded sandboxes. `admission.code` is only a
126
+ fallback — an executor implementing `acquire()` owns a bounded pool already, its
127
+ settings win, and connecta warns the fallback was ignored. Invalid bounds throw
128
+ at construction, because a pool that quietly became unbounded is worse than a
129
+ deployment that refuses to boot. The queue is global FIFO across identities: a
130
+ capacity boundary, not tenant fairness, and one deployment serves one tenant.
131
+
132
+ Call admission (`src/call-admission.ts`) answers what the envelope cannot see —
133
+ a connector's optional policy over its own `Connector.callTool` attempts,
134
+ partitioned by an optional `partitionKey` and bounded by concurrency, a
135
+ rolling-window budget, or both. Exactly one rule is accepted, because several
136
+ cannot be faked as sequential leases: consuming a rolling token before a later
137
+ rule refuses would charge a call that never reached the provider, the exact
138
+ accounting error a budget exists to prevent. Both layers are pinned by
139
+ `test/request-admission.test.ts` and `test/call-admission.test.ts`.
140
+
141
+ ## Storage, credentials, and connectors
142
+
143
+ `KVStorage` is `get`/`set`/`delete` with optional `list(prefix)`; core uses it for
144
+ connector state, catalogs, and result paging — a 15-minute TTL with one
145
+ runtime-wide accounting of stash bytes and entries, where a full stash returns the
146
+ successful call's preview and a paging-unavailable notice rather than a result id.
147
+ Adapters: `src/storage/memory.ts`, `src/storage/file.ts` (Node), and the
148
+ Cloudflare KV/D1 pair in `examples/worker/`, copyable reference source and
149
+ deliberately not an importable subpath.
150
+
151
+ `src/credentials.ts` is the AES-GCM vault behind the root-exported
152
+ `CredentialVault` contract, selected through the `vault` slot. It binds connector
153
+ id and owner into the authenticated encryption context, because sharing a backend
154
+ is not permission to share a principal's credentials. Two rules carry the
155
+ subsystem: credentials never leave the host — read only through the owning
156
+ connector's `ctx.credential`, rendered by nothing, absent from activity and model
157
+ recovery — and they fail at use, proactive liveness probing having been removed by
158
+ decision. The vault is read per call, so a replacement needs no restart
159
+ (`test/credentials.test.ts`).
160
+
161
+ Connectors are the boundary between the fixed meta-tool surface and downstream
162
+ capability, and `api()`, `remoteMcp()`, and a hand-written `Connector`
163
+ (`src/connectors/`, plus the prebuilt connections under `src/providers/`) all
164
+ produce instances that take the same catalog, read-only, credential, storage,
165
+ invocation, result-size, and activity paths. Every one is deployment
166
+ configuration, never runtime registration. `authScope: "shared" | "personal"`
167
+ partitions connecta-owned context — state, credentials, OAuth, catalogs, observed
168
+ shapes — by principal, and *only* connecta-owned context: a secret a custom
169
+ handler closes over is shared JavaScript state, and `remoteMcp()` refuses the
170
+ literal-headers-plus-personal version of that mistake. Visibility
171
+ (`identity.connectorAccess`) is a separate rule; hiding a connector does not
172
+ change who owns its auth.
146
173
 
147
174
  ## Optional deployment modules
148
175
 
149
- `createConnecta` takes closed typed `ui`, `vault`, and `activity` slots. Factories
150
- live at `/ui`, `/credentials`, and `/activity`; bearer auth lives at
151
- `/auth/bearer`. Root exports the contracts, never these implementations. There
152
- is no module array, runtime registration, or plugin lifecycle.
153
-
154
- Core keeps connector discovery, the executor contract, invocation, permissions,
155
- and OAuth callback verification together. Optional modules contribute no
156
- runtime work when omitted. The UI supplies credential handoff URLs only while
157
- mounted. Status reads never initiate OAuth, and each lazy details request owns
158
- its downstream scope. See [operator UI](./operator-ui.md).
176
+ `createConnecta` takes closed typed `ui`, `vault`, and `activity` slots, with
177
+ factories at `/ui`, `/credentials`, and `/activity` and bearer auth at
178
+ `/auth/bearer`. Root exports the contracts, never the implementations, and there
179
+ is no module array, runtime registration, or plugin lifecycle. Core keeps
180
+ discovery, the executor contract, invocation, permissions, and OAuth callback
181
+ verification; an omitted module contributes no runtime work at all.
182
+
183
+ The operator UI — `src/ui.ts` (data-free shell and `/ui/data` payload),
184
+ `src/routes/ui.ts`, `src/operator-ui/` (the Preact app and its pure rules)
185
+ shows a human what a deployment exposes and manages only the authentication
186
+ material code explicitly permitted; it never edits the connector set, catalog,
187
+ annotations, scopes, or permission rules. Two invariants shape it: a status read
188
+ never starts authorization, since OAuth begins with an explicit authorized POST,
189
+ and each lazy details request owns a bounded downstream scope, so one failing
190
+ provider leaves the other connections usable. Credential handoff URLs exist only
191
+ while the UI is mounted; OAuth callbacks never need it.
192
+
193
+ Its appearance is one token layer. `src/operator-ui/browser.css` resolves every
194
+ color, radius, and font through a custom property and mixes the rest from those
195
+ with `color-mix`, so `branding.theme` only has to append a `:root` block after
196
+ that stylesheet. The five tokens it accepts are gated in `src/branding.ts`, each
197
+ by a narrow syntactic check: deployment config reaches a `<style>` element here,
198
+ and an unvalidated value would be CSS injection. The dark palette is the same
199
+ tokens under `prefers-color-scheme`; `colorScheme` pins one with a `data-scheme`
200
+ attribute on the page.
159
201
 
160
202
  ## Import-graph purity
161
203
 
162
- Nothing reachable from `src/index.ts` may import a `node:` builtin. The core is
163
- Web-API only so the same code runs unchanged in workerd and in Node.
164
-
165
- The Node-touching paths are `src/node.ts` (the `node:http` adapter),
166
- `src/storage/file.ts`, and the QuickJS process pool
167
- (`src/executors/quickjs.ts` and its child entry). Each lives behind an explicit
168
- subpath export `@zackbart/connecta/node`, `@zackbart/connecta/quickjs` and
169
- must stay unreachable from the root entry. The optional Clerk adapter is behind
170
- `./auth/clerk` for the adjacent reason: `@clerk/backend` is an optional peer,
171
- not a dependency. The zero-dependency Cloudflare Access adapter likewise stays
172
- behind `./auth/cloudflare-access`: it is Web-API-pure, but its trust contract is
173
- specific to a direct Worker invocation carrying `ctx.access`.
174
-
175
- `test/purity.test.ts` walks the relative-import graph from `src/index.ts` and
176
- fails on (a) any `node:` specifier in a reachable file and (b) the Node
177
- adapter, file storage, QuickJS parent or child, auth adapters, UI bundle,
178
- encrypted vault implementation, or activity implementation being reachable at all. `test/package-surface.test.ts` and
179
- `scripts/check-package.mjs` guard the other half — that the published tarball
180
- matches the same boundary.
181
-
182
- The failure mode this prevents is not theoretical: a single convenience import
183
- of `node:crypto` in a shared helper makes the whole Worker deployment shape
184
- stop building, and it will do so in someone else's repository rather than
185
- this one.
186
-
187
- ## Where things live
204
+ Nothing reachable from `src/index.ts` may import a `node:` builtin, so the same
205
+ core runs unchanged in workerd and in Node. The Node-touching paths — `src/node.ts`
206
+ (the `node:http` adapter), `src/storage/file.ts`, and the QuickJS process pool
207
+ (`src/executors/quickjs.ts` plus its child) — each sit behind an explicit subpath
208
+ and must stay unreachable from the root. `./auth/clerk` is separate because
209
+ `@clerk/backend` is an optional peer rather than a dependency, and
210
+ `./auth/cloudflare-access` for a third reason: it is Web-API-pure, but its trust
211
+ contract is specific to a direct Worker invocation carrying `ctx.access`.
212
+
213
+ `test/purity.test.ts` walks the relative-import graph and fails on any `node:`
214
+ specifier in a reachable file, or on any of those modules — plus the UI bundle,
215
+ encrypted vault, and activity implementation being reachable at all;
216
+ `test/package-surface.test.ts` and `scripts/check-package.mjs` guard the same
217
+ boundary in the published tarball. The failure mode is not theoretical: one
218
+ convenience import of `node:crypto` in a shared helper stops the whole Worker
219
+ shape from building, in someone else's repository rather than this one.
220
+
221
+ ## Where else to look
222
+
223
+ Beyond the modules already named:
188
224
 
189
225
  ```
190
226
  src/
191
- index.ts createConnecta + the public re-exports (Workers-clean)
192
227
  server.ts route ordering, HTTPS upgrade, security wrapper
193
228
  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
229
  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
230
  catalog-drift.ts vetted manifests and the counts a refresh produces
203
- credentials.ts the AES-GCM connector vault over KVStorage
204
231
  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
232
  auth/ bearer, Cloudflare Access, clerk (optional peer), downstream OAuth
212
233
  executors/ the QuickJS pool and child (Node only)
213
- storage/ memory.ts, file.ts (Node only)
214
234
  node.ts listen() + fileStorage re-export (Node only)
215
235
  ```
216
236
 
237
+ There are exactly two deployment shapes — `templates/node/`, which
238
+ `connecta init` copies with its container files, and `examples/worker/` — and
239
+ `test/deployment-shapes.test.ts` with `npm run check:examples` keeps both
240
+ compiling and configuring the real thing.
241
+
217
242
  ## Sharp edges
218
243
 
219
244
  - **The root registry is shared; identity views are partitioned.** Shared
220
- connector caches are visible to later requests in the isolate. Personal
221
- connectors use a bounded principal registry, and transient results use the
222
- authenticated subject. Anything cached per request still dies with it.
223
- Putting a downstream client or credential on the wrong side of those lines
224
- is the highest-severity mistake available here.
225
- - **Route order is behavior.** Moving a mutation route below the
226
- wildcard `OPTIONS` opts it into CORS preflight.
227
- - **Admission runs before auth, on purpose.** Reordering them to "authenticate
228
- first" makes the cheapest possible attack the most expensive request.
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
245
+ connector caches are visible to later requests in the isolate; personal
246
+ connectors use a bounded principal registry and transient results the
247
+ authenticated subject. Putting a downstream client or credential on the wrong
248
+ side of those lines is the highest-severity mistake available here.
249
+ - **Route order is behavior.** Moving a mutation route below the wildcard
250
+ `OPTIONS` opts it into CORS preflight; reordering admission after auth makes
251
+ the cheapest possible attack the most expensive request.
252
+ - **`close()` is idempotent and ordered.** Both admission pools, then the
253
+ connector limiters, then the executor; Node's `listen()` calls it on
231
254
  SIGTERM/SIGINT.
232
255
  - **Structural mistakes throw at construction.** A duplicate connector id, an
233
- invalid admission rule, removed `accessTokens` option, or missing executor: all refuse to boot. A deployment that starts in the wrong shape is
234
- worse than one that does not start.
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` |
256
+ invalid admission rule, the removed `accessTokens` option, a missing executor:
257
+ all refuse to boot (`test/config.test.ts`, `test/registry.test.ts`). Starting
258
+ in the wrong shape is worse than not starting.