@zackbart/connecta 0.24.2 → 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.
- package/AGENTS.md +18 -20
- package/CHANGELOG.md +205 -1
- package/README.md +5 -6
- package/dist/auth/bearer.js +2 -0
- package/dist/auth/downstream-oauth.d.ts +12 -1
- package/dist/auth/downstream-oauth.js +147 -35
- package/dist/branding.d.ts +31 -2
- package/dist/branding.js +116 -8
- package/dist/call-admission.d.ts +4 -0
- package/dist/call-admission.js +26 -0
- package/dist/catalog-drift.js +9 -4
- package/dist/catalog-service.d.ts +2 -0
- package/dist/catalog-service.js +25 -8
- package/dist/catalog.d.ts +2 -0
- package/dist/catalog.js +246 -121
- package/dist/connectors/api.d.ts +1 -1
- package/dist/connectors/api.js +21 -3
- package/dist/connectors/guarded-fetch.d.ts +6 -2
- package/dist/connectors/guarded-fetch.js +61 -24
- package/dist/connectors/remote-mcp.js +92 -57
- package/dist/errors.d.ts +28 -3
- package/dist/errors.js +60 -1
- package/dist/execute.js +85 -23
- package/dist/executor-result.js +3 -1
- package/dist/executors/quickjs-child.js +5 -1
- package/dist/executors/quickjs-protocol.d.ts +4 -0
- package/dist/executors/quickjs-runtime.d.ts +1 -1
- package/dist/executors/quickjs-runtime.js +38 -21
- package/dist/executors/quickjs.js +68 -27
- package/dist/index.d.ts +15 -1
- package/dist/index.js +36 -4
- package/dist/invocation.js +134 -93
- package/dist/mcp-result.js +3 -2
- package/dist/meta-tools.js +191 -36
- package/dist/operator-ui/generated.js +2 -2
- package/dist/operator-ui/view.d.ts +38 -1
- package/dist/operator-ui/view.js +71 -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 +83 -10
- package/dist/providers/revenuecat.d.ts +30 -4
- package/dist/providers/revenuecat.js +42 -4
- package/dist/providers/stripe.d.ts +7 -1
- package/dist/providers/stripe.js +30 -4
- package/dist/providers/vercel.js +11 -1
- package/dist/registry.d.ts +22 -2
- package/dist/registry.js +101 -13
- package/dist/routes/mcp.d.ts +4 -1
- package/dist/routes/mcp.js +84 -13
- package/dist/routes/oauth.js +4 -0
- package/dist/routes/shared.d.ts +1 -0
- package/dist/routes/shared.js +4 -4
- package/dist/server.js +15 -3
- package/dist/skills.js +6 -5
- package/dist/storage/file.d.ts +6 -2
- package/dist/storage/file.js +312 -34
- package/dist/storage/memory.js +12 -1
- package/dist/types.d.ts +37 -0
- package/dist/ui.js +18 -10
- package/dist/validate.js +3 -3
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/documentation/architecture.md +193 -165
- package/documentation/auth.md +199 -145
- package/documentation/code-mode.md +433 -316
- package/documentation/meta-tools.md +363 -355
- package/examples/worker/AGENTS.md +2 -1
- package/examples/worker/README.md +12 -10
- package/examples/worker/src/index.ts +12 -15
- package/package.json +1 -2
- 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/documentation/call-admission.md +0 -142
- package/documentation/cloudflare.md +0 -471
- package/documentation/connector-guides.md +0 -176
- package/documentation/connectors.md +0 -389
- 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 -722
- package/documentation/request-admission.md +0 -170
- package/documentation/revenuecat.md +0 -305
- package/documentation/storage-and-credentials.md +0 -201
- package/documentation/stripe.md +0 -262
- package/documentation/upgrading.md +0 -754
- package/documentation/vercel.md +0 -241
package/dist/validate.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Validator } from "@cfworker/json-schema";
|
|
2
|
-
import { ConnectorCallError } from "./errors.js";
|
|
2
|
+
import { boundedEchoText, ConnectorCallError } from "./errors.js";
|
|
3
3
|
import { MAX_ARGUMENT_VALIDATION_ISSUES } from "./errors.js";
|
|
4
4
|
// Lazy validator cache keyed by the schema object itself; null marks a schema
|
|
5
5
|
// the validator rejected (warned once, then passed through rather than
|
|
@@ -250,10 +250,10 @@ export function validateToolInput(schema, args, opts) {
|
|
|
250
250
|
if (result && !result.valid) {
|
|
251
251
|
const units = normalizedValidationUnits(schema, result.errors);
|
|
252
252
|
const nestedUnits = units.filter((unit) => unit.instanceLocation !== "#");
|
|
253
|
-
const detail = (nestedUnits.length > 0 ? nestedUnits : units)
|
|
253
|
+
const detail = boundedEchoText((nestedUnits.length > 0 ? nestedUnits : units)
|
|
254
254
|
.slice(0, MAX_ARGUMENT_VALIDATION_ISSUES)
|
|
255
255
|
.map((unit) => `${unit.instanceLocation}: ${agentFacingValidationError(unit)}`)
|
|
256
|
-
.join("; ");
|
|
256
|
+
.join("; "), 256);
|
|
257
257
|
return new ConnectorCallError("invalid_args", `Invalid arguments for "${opts.address}": ${detail || "input does not match the tool's inputSchema"}`, { validation: validationDetails(schema, units) });
|
|
258
258
|
}
|
|
259
259
|
return null;
|
package/dist/version.d.ts
CHANGED
package/dist/version.js
CHANGED
|
@@ -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,218 +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 }
|
|
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
|
-
generation retirement removes it along with the map entry. It never retains a
|
|
35
|
-
token response, client, or transport. If cancellation lands after a valid
|
|
36
|
-
response while its credential write is still running, a generation-keyed
|
|
37
|
-
identity marker rejects new owners until that exact write finishes. The marker
|
|
38
|
-
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)).
|
|
39
35
|
|
|
40
36
|
**Per request, and no longer.** The MCP server, its transport, downstream MCP
|
|
41
37
|
clients, abort signals, and the connector scope a probe opens all belong to the
|
|
42
|
-
request that created them. `Nothing request-bound survives a request` is an
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
The registry is deliberately on the long side of that line and the MCP server
|
|
53
|
-
deliberately on the short side. A fresh `McpServer` per request is what makes
|
|
54
|
-
the deployment stateless: no sessions, no server push, no resumability, and
|
|
55
|
-
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.
|
|
56
47
|
|
|
57
48
|
## Request lifecycle
|
|
58
49
|
|
|
59
|
-
`src/server.ts` is the composition root
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
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.
|
|
64
54
|
|
|
65
55
|
| Order | Route | Notes |
|
|
66
56
|
| --- | --- | --- |
|
|
67
|
-
| 0 |
|
|
68
|
-
| 0 |
|
|
69
|
-
|
|
|
70
|
-
|
|
|
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. |
|
|
71
63
|
| 3 | `/.well-known/*` | Auth metadata, or 404. |
|
|
72
|
-
| 4 | `/health` | Open payload-free health, executor, admission, and deployment metadata
|
|
73
|
-
| 5 | `/oauth/callback/<connectorId>` | Core downstream OAuth completion, state
|
|
74
|
-
| 6 | `/mcp`, `/mcp/<pool>` | Admission
|
|
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). |
|
|
75
67
|
| 7 | Other paths | 404. Custom HTTP routes belong to the deployment. |
|
|
76
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.
|
|
77
74
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
and the exact refusal bodies; it exists because the ordering is invisible in
|
|
81
|
-
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`:
|
|
82
77
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
so an unauthenticated flood costs a permit rather than a Clerk lookup
|
|
87
|
-
([request admission](./request-admission.md)). The permit is held until the
|
|
88
|
-
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.
|
|
89
81
|
2. **Authorize.** Each `InboundAuth` provider's `authorize` in order, bearer
|
|
90
|
-
before interactive
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
3. **
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
6. **Serve.** A fresh `McpServer` per request, the seven meta-tools registered
|
|
107
|
-
against the registry and the response
|
|
108
|
-
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`).
|
|
109
98
|
|
|
110
99
|
## Layers below the meta-tools
|
|
111
100
|
|
|
112
|
-
The meta-tool handlers are thin. The work sits in
|
|
113
|
-
|
|
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:
|
|
114
103
|
|
|
115
104
|
| Module | Owns |
|
|
116
105
|
| --- | --- |
|
|
117
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. |
|
|
118
|
-
| `src/catalog-service.ts` | Request-local
|
|
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. |
|
|
119
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. |
|
|
120
109
|
| `src/catalog.ts` | Ranking, description summarizing, and the compact schema renderer discovery shows. |
|
|
121
110
|
| `src/result-shapes.ts` | Bounded runtime-only inference and merging for output shapes learned from successful read-only calls whose providers declared none. |
|
|
122
111
|
|
|
123
|
-
`src/meta-tools.ts` and `src/execute.ts` are two front doors onto the same
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
the
|
|
129
|
-
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.
|
|
130
173
|
|
|
131
174
|
## Optional deployment modules
|
|
132
175
|
|
|
133
|
-
`createConnecta` takes closed typed `ui`, `vault`, and `activity` slots
|
|
134
|
-
|
|
135
|
-
`/auth/bearer`. Root exports the contracts, never
|
|
136
|
-
is no module array, runtime registration, or plugin lifecycle.
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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.
|
|
143
201
|
|
|
144
202
|
## Import-graph purity
|
|
145
203
|
|
|
146
|
-
Nothing reachable from `src/index.ts` may import a `node:` builtin
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
The failure mode this prevents is not theoretical: a single convenience import
|
|
167
|
-
of `node:crypto` in a shared helper makes the whole Worker deployment shape
|
|
168
|
-
stop building, and it will do so in someone else's repository rather than
|
|
169
|
-
this one.
|
|
170
|
-
|
|
171
|
-
## 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:
|
|
172
224
|
|
|
173
225
|
```
|
|
174
226
|
src/
|
|
175
|
-
index.ts createConnecta + the public re-exports (Workers-clean)
|
|
176
227
|
server.ts route ordering, HTTPS upgrade, security wrapper
|
|
177
228
|
routes/ one file per surface; shared.ts holds the auth gate
|
|
178
|
-
meta-tools.ts the six non-execute meta-tools over the registry
|
|
179
|
-
execute.ts execute_code, the sandbox host bridge, emitted media
|
|
180
229
|
skills.ts MCP instructions, the usage skill, connector guides
|
|
181
|
-
registry.ts connector set, addresses, health, call limiters
|
|
182
|
-
catalog-service.ts request-local catalog access, search, and describe
|
|
183
|
-
catalog.ts ranking, summaries, compact schema rendering
|
|
184
|
-
result-shapes.ts passive runtime-only observed output schemas
|
|
185
|
-
invocation.ts one tool call, end to end
|
|
186
230
|
catalog-drift.ts vetted manifests and the counts a refresh produces
|
|
187
|
-
credentials.ts the AES-GCM connector vault over KVStorage
|
|
188
231
|
activity.ts optional history factory and best-effort recorder
|
|
189
|
-
call-admission.ts connector-partitioned downstream permits and budgets
|
|
190
|
-
executor-admission.ts the portable bounded queue both pools use
|
|
191
|
-
ui.ts the served operator shell and /ui/data payload
|
|
192
|
-
operator-ui/ the Preact app, its pure rules, and the built bundle
|
|
193
|
-
connectors/ remote-mcp.ts, api.ts, guarded-fetch.ts
|
|
194
|
-
providers/ the maintained prebuilt connections
|
|
195
232
|
auth/ bearer, Cloudflare Access, clerk (optional peer), downstream OAuth
|
|
196
233
|
executors/ the QuickJS pool and child (Node only)
|
|
197
|
-
storage/ memory.ts, file.ts (Node only)
|
|
198
234
|
node.ts listen() + fileStorage re-export (Node only)
|
|
199
235
|
```
|
|
200
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
|
+
|
|
201
242
|
## Sharp edges
|
|
202
243
|
|
|
203
244
|
- **The root registry is shared; identity views are partitioned.** Shared
|
|
204
|
-
connector caches are visible to later requests in the isolate
|
|
205
|
-
connectors use a bounded principal registry
|
|
206
|
-
authenticated subject.
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
-
|
|
212
|
-
|
|
213
|
-
- **`close()` is idempotent and ordered.** It closes both admission pools and
|
|
214
|
-
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
|
|
215
254
|
SIGTERM/SIGINT.
|
|
216
255
|
- **Structural mistakes throw at construction.** A duplicate connector id, an
|
|
217
|
-
invalid admission rule, removed `accessTokens` option,
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
## Tests that enforce this
|
|
221
|
-
|
|
222
|
-
| Invariant | Suite |
|
|
223
|
-
| --- | --- |
|
|
224
|
-
| The core imports no `node:` builtin and reaches no Node-only module | `test/purity.test.ts` |
|
|
225
|
-
| The published surface matches the same boundary | `test/package-surface.test.ts`, `scripts/check-package.mjs` |
|
|
226
|
-
| Route order, per-route auth, and byte-exact refusals | `test/server-route-contracts.test.ts` |
|
|
227
|
-
| `/mcp` end to end, the open routes, exactly seven tools, bounded connector orientation | `test/server.test.ts`, `test/code-first-surface.test.ts` |
|
|
228
|
-
| Construction-time refusals and the grouped config boundary | `test/config.test.ts`, `test/registry.test.ts` |
|
|
229
|
-
| Program and top-level calls take the same enforced path | `test/execute.test.ts` |
|
|
230
|
-
| 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.
|