@zackbart/connecta 0.32.0 → 0.34.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/CHANGELOG.md +98 -0
- package/README.md +95 -116
- package/SECURITY.md +1 -1
- package/bin/connecta.mjs +24 -6
- package/bin/version.mjs +1 -1
- package/dist/access-tokens.d.ts +167 -23
- package/dist/access-tokens.js +676 -140
- package/dist/activity.d.ts +3 -0
- package/dist/agent-identity.js +47 -0
- package/dist/auth/cloudflare-access.js +13 -5
- package/dist/branding.d.ts +7 -0
- package/dist/branding.js +76 -9
- package/dist/call-admission-context.js +15 -0
- package/dist/call-admission.js +30 -43
- package/dist/call-recovery.js +6 -2
- package/dist/catalog-service.js +15 -1
- package/dist/config-value-sources.js +16 -9
- package/dist/config.d.ts +48 -75
- package/dist/config.js +144 -36
- package/dist/connector-access.d.ts +4 -34
- package/dist/connector-access.js +15 -99
- package/dist/connector-caller.d.ts +7 -1
- package/dist/connector-caller.js +4 -2
- package/dist/connectors/api-connector.d.ts +27 -4
- package/dist/connectors/api-connector.js +2 -0
- package/dist/connectors/remote-mcp.js +2 -0
- package/dist/describe-config.d.ts +31 -7
- package/dist/describe-config.js +8 -17
- package/dist/execute.js +1 -11
- package/dist/fix-prompt.js +2 -2
- package/dist/index.d.ts +4 -3
- package/dist/index.js +30 -10
- package/dist/invocation.js +5 -3
- package/dist/membership.d.ts +53 -0
- package/dist/membership.js +121 -0
- package/dist/module-contracts.d.ts +23 -6
- package/dist/oauth-callback-outcome.js +2 -2
- package/dist/operator-record.d.ts +2 -2
- package/dist/operator-record.js +2 -4
- package/dist/operator-ui/agents.d.ts +74 -0
- package/dist/operator-ui/agents.js +104 -0
- package/dist/operator-ui/config-summary.js +562 -0
- package/dist/operator-ui/connection-map.js +375 -0
- package/dist/operator-ui/contract.d.ts +149 -28
- package/dist/operator-ui/endpoints.js +221 -0
- package/dist/operator-ui/fix-prompts.js +5 -5
- package/dist/operator-ui/generated.js +5 -4
- package/dist/operator-ui/member-page.js +97 -0
- package/dist/operator-ui/model.d.ts +14 -6
- package/dist/operator-ui/overview.js +134 -0
- package/dist/operator-ui/view.d.ts +107 -37
- package/dist/operator-ui/view.js +163 -86
- package/dist/page-styles.js +5 -3
- package/dist/provider.d.ts +61 -8
- package/dist/provider.js +46 -7
- package/dist/providers/_shared/google/workspace.d.ts +56 -17
- package/dist/providers/_shared/google/workspace.js +34 -8
- package/dist/providers/_shared/rest/tools.js +2 -2
- package/dist/providers/_shared/rest/value-safety.d.ts +50 -0
- package/dist/providers/_shared/rest/value-safety.js +699 -7
- package/dist/providers/ccb/index.js +2 -2
- package/dist/providers/cloudflare/index.js +3 -3
- package/dist/providers/cloudflare/rest.js +25 -3
- package/dist/providers/cloudflare/skill.generated.js +1 -1
- package/dist/providers/cloudflare/value-safety.js +32 -0
- package/dist/providers/docs/index.js +4 -4
- package/dist/providers/docs/skill.generated.js +2 -2
- package/dist/providers/drive/index.js +4 -4
- package/dist/providers/drive/skill.generated.js +2 -2
- package/dist/providers/forms/index.js +4 -4
- package/dist/providers/forms/skill.generated.js +2 -2
- package/dist/providers/gmail/index.js +11 -9
- package/dist/providers/gmail/skill.generated.js +2 -2
- package/dist/providers/mixpanel/index.js +1 -1
- package/dist/providers/mixpanel/skill.generated.js +3 -3
- package/dist/providers/notion/index.d.ts +7 -0
- package/dist/providers/notion/index.js +100 -11
- package/dist/providers/notion/rest.js +45 -5
- package/dist/providers/notion/skill.generated.js +2 -2
- package/dist/providers/notion/value-safety.js +30 -5
- package/dist/providers/overflow/index.js +2 -2
- package/dist/providers/overflow/skill.generated.js +2 -2
- package/dist/providers/planning-center/index.js +2 -2
- package/dist/providers/planning-center/skill.generated.js +4 -4
- package/dist/providers/revenuecat/skill.generated.js +2 -2
- package/dist/providers/sheets/index.d.ts +3 -0
- package/dist/providers/sheets/index.js +7 -4
- package/dist/providers/sheets/skill.generated.js +2 -2
- package/dist/providers/slides/index.js +4 -4
- package/dist/providers/slides/skill.generated.js +2 -2
- package/dist/providers/stripe/rest.js +9 -2
- package/dist/providers/stripe/skill.generated.js +1 -1
- package/dist/providers/stripe/value-safety.js +22 -0
- package/dist/providers/tithely/index.js +1 -1
- package/dist/providers/vercel/index.js +1 -1
- package/dist/providers/vercel/rest.js +20 -2
- package/dist/providers/vercel/skill.generated.js +2 -2
- package/dist/providers/vercel/value-safety.js +22 -0
- package/dist/registry.d.ts +2 -3
- package/dist/registry.js +23 -9
- package/dist/routes/access-tokens.js +82 -75
- package/dist/routes/activity-disclosure.js +17 -18
- package/dist/routes/activity.js +34 -54
- package/dist/routes/actor-labels.js +33 -4
- package/dist/routes/agent-path.js +43 -0
- package/dist/routes/connect.js +16 -12
- package/dist/routes/credentials.js +16 -40
- package/dist/routes/mcp.js +161 -104
- package/dist/routes/oauth-management.js +11 -5
- package/dist/routes/oauth.js +5 -11
- package/dist/routes/operator.js +24 -23
- package/dist/routes/shared.d.ts +60 -20
- package/dist/routes/shared.js +187 -96
- package/dist/routes/ui-config.js +437 -97
- package/dist/routes/ui-data.js +2 -2
- package/dist/routes/ui.js +9 -16
- package/dist/runtime/call-admission.js +9 -0
- package/dist/server.js +5 -0
- package/dist/skills.js +10 -0
- package/dist/storage/keys.js +9 -1
- package/dist/storage/sql.js +5 -1
- package/dist/types.d.ts +29 -7
- package/dist/ui.d.ts +1 -1
- package/dist/ui.js +23 -16
- package/dist/usage-guide.js +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/workspace-address.js +9 -0
- package/dist/workspace-subject.js +23 -0
- package/documentation/README.md +47 -23
- package/documentation/architecture.md +371 -61
- package/documentation/auth.md +897 -249
- package/documentation/code-mode.md +37 -16
- package/documentation/deploying.md +126 -25
- package/documentation/integrating.md +97 -23
- package/documentation/meta-tools.md +50 -31
- package/documentation/migration-0.33.md +352 -0
- package/documentation/migration-0.34.md +67 -0
- package/documentation/operating.md +3 -1
- package/documentation/operator-tests.md +147 -34
- package/documentation/operator-ui.md +519 -64
- package/documentation/provider-auth-migration.md +37 -23
- package/documentation/provider-migration-0.29.md +2 -2
- package/examples/worker/AGENTS.md +37 -9
- package/examples/worker/README.md +127 -61
- package/examples/worker/src/connecta.config.ts +40 -23
- package/examples/worker/wrangler.jsonc +4 -1
- package/package.json +19 -18
- package/templates/node/.env.example +3 -0
- package/templates/node/AGENTS.md +33 -6
- package/templates/node/Dockerfile +2 -2
- package/templates/node/README.md +80 -52
- package/templates/node/docker-compose.yml +3 -2
- package/templates/node/package.json +4 -3
- package/templates/node/src/adopt-token.ts +23 -0
- package/templates/node/src/connecta.config.ts +28 -12
- package/templates/node/src/rotate-token.ts +17 -0
- package/templates/node/src/provision-token.ts +0 -17
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Architecture
|
|
2
2
|
|
|
3
|
+
## Contributor reference
|
|
4
|
+
|
|
5
|
+
This guide describes Connecta implementation and repository verification.
|
|
6
|
+
It is optional for deployment operators. Start with the
|
|
7
|
+
[operator task index](./README.md#operator-tasks) for package-local procedures.
|
|
8
|
+
|
|
3
9
|
One Web-standard `fetch(request) => Promise<Response>` handler, a long-lived
|
|
4
10
|
registry behind it, and a strict rule about what may be imported. Everything
|
|
5
11
|
else in this repository is a detail of those three things.
|
|
@@ -85,20 +91,21 @@ table, security headers. Route _order_ is the contract — several routes would
|
|
|
85
91
|
behave differently if they were reachable in another order — so read the table
|
|
86
92
|
top to bottom.
|
|
87
93
|
|
|
88
|
-
| Order | Route | Notes
|
|
89
|
-
| ----- | --------------------------------------------------------- |
|
|
90
|
-
| 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.
|
|
91
|
-
| 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.
|
|
92
|
-
| 0 | Cloudflare Access (Worker, when enabled) | Edge admission ahead of this table; an admitted invocation carries trusted identity in `ctx.access`.
|
|
93
|
-
| 1 | `/
|
|
94
|
-
| 1 |
|
|
95
|
-
| 1 |
|
|
96
|
-
|
|
|
97
|
-
| 2 |
|
|
98
|
-
|
|
|
99
|
-
|
|
|
100
|
-
|
|
|
101
|
-
|
|
|
94
|
+
| Order | Route | Notes |
|
|
95
|
+
| ----- | --------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
96
|
+
| 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. |
|
|
97
|
+
| 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. |
|
|
98
|
+
| 0 | Cloudflare Access (Worker, when enabled) | Edge admission ahead of this table; an admitted invocation carries trusted identity in `ctx.access`. |
|
|
99
|
+
| 1 | Agent namespace, any path starting `/agent` once decoded | First, so no path Access bypasses reaches a human route. Exactly `/agent/<pool>`: admission, then the `cta_` adapter alone, then pool membership; every other spelling is the pool 404 before admission or auth. See [agent endpoints](./auth.md#agent-endpoints). |
|
|
100
|
+
| 1 | `/oauth/client-metadata/<connectorId>` | Public GET-only CIMD for configured self-hosted OAuth connectors; fixed `publicUrl`, no auth or storage. |
|
|
101
|
+
| 1 | Mounted UI routes | Before wildcard OPTIONS, so mutation routes refuse preflight rather than inheriting MCP CORS. No UI module, no routes. |
|
|
102
|
+
| 1 | `/connect/<connectorId>`, `/oauth/callback/<connectorId>` | GET-only browser OAuth routes, before wildcard OPTIONS. Both verify the initiating user and management permission, independent of the UI. |
|
|
103
|
+
| 2 | MCP preflight | Allowed `OPTIONS` on `/mcp*`: 204 without admission or auth. |
|
|
104
|
+
| 2 | Other `OPTIONS` | Auth metadata first, otherwise compatibility CORS preflight. |
|
|
105
|
+
| 3 | `/.well-known/*` | Public auth metadata, or 404. Protected-resource discovery does not authenticate callers or evaluate endpoint grants. |
|
|
106
|
+
| 4 | `/health` | Open and payload-free: health, executor, admission, and deployment metadata. |
|
|
107
|
+
| 6 | `/mcp`, `/mcp/<pool>` | Admission, then auth, then a request-local MCP server. No body is read before auth and endpoint admission. A request naming `subscriptions/listen` takes its permit only after a bounded classification read (413 past 64 KiB); a body-confirmed modern listen takes none and is refused after SDK validation. A non-maintainer at `/mcp`, a non-member, and an undeclared pool get one identical 404; see [pools](./auth.md#pools). |
|
|
108
|
+
| 7 | Other paths | 404. Custom HTTP routes belong to the deployment. |
|
|
102
109
|
|
|
103
110
|
Every response leaves through `withSecurityHeaders`. HTML uses one common
|
|
104
111
|
security-header helper, including status and error pages. Operator shells
|
|
@@ -112,36 +119,46 @@ reordering reads like a harmless refactor.
|
|
|
112
119
|
An admitted non-preflight `/mcp` request then takes five steps in
|
|
113
120
|
`src/routes/mcp.ts`:
|
|
114
121
|
|
|
115
|
-
1. **Admit.**
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
122
|
+
1. **Admit.** No request body is read before auth on any endpoint. A request
|
|
123
|
+
whose `Mcp-Method` header names `subscriptions/listen` takes no permit
|
|
124
|
+
before auth. After auth and endpoint admission connecta reads at most
|
|
125
|
+
64 KiB (`MAX_LISTEN_CLASSIFICATION_BYTES`) of it: past that it answers
|
|
126
|
+
413 `-33006` and stops reading. A body that confirms a modern listen stays
|
|
127
|
+
permit-free, passes SDK validation, and receives HTTP 404 with JSON-RPC
|
|
128
|
+
`-32601`; anything else takes a permit then, so the header alone bypasses
|
|
129
|
+
nothing. Auth and classification share the configured request lifetime.
|
|
130
|
+
Other requests take one permit from the deployment-wide pool, taken before
|
|
131
|
+
auth so an unauthenticated flood costs a permit rather than a Clerk lookup,
|
|
132
|
+
and held until the response _body_ completes, the caller leaves, or its
|
|
133
|
+
configured lifetime ends, not until the handler returns.
|
|
134
|
+
2. **Authorize.** `/agent/<pool>` consults only the built-in `cta_` adapter
|
|
135
|
+
(`authorizeAgent`), never with the runtime context that carries
|
|
136
|
+
`ctx.access`. Human endpoints refuse a `cta_` token before recognition or
|
|
137
|
+
storage, and never ask the `cta_` adapter. A provider's
|
|
125
138
|
synchronous credential recognition selects the sole verifier for an explicit
|
|
126
|
-
credential; a refusal never falls back to another identity.
|
|
127
|
-
reject machine credentials without verification. With no recognized
|
|
139
|
+
credential; a refusal never falls back to another identity. With no recognized
|
|
128
140
|
credential, configured providers are tried in order. A 401 takes its
|
|
129
141
|
challenge from the actual protected-resource metadata owner. No providers
|
|
130
142
|
means open development and warns at construction.
|
|
131
|
-
3. **
|
|
132
|
-
|
|
133
|
-
|
|
143
|
+
3. **Admit to the endpoint.** Plain `/mcp` admits a principal listed in
|
|
144
|
+
`maintainers` (or, with no inbound auth, the anonymous caller) and serves the
|
|
145
|
+
whole catalog. `/mcp/<pool>` admits the pool's human `members`, matched by
|
|
146
|
+
principal, and `/agent/<pool>` its agent members, matched by
|
|
147
|
+
`identity.agent`; both serve exactly the pool's tools
|
|
148
|
+
(`src/membership.ts`, `admitsMaintainer`, `memberPool`, and `agentPool` in
|
|
149
|
+
`src/routes/shared.ts`). Nothing intersects or widens either view; every
|
|
150
|
+
refusal is one 404 naming no pool.
|
|
134
151
|
The endpoint's `readOnly` is the pool's own value, else the deployment's;
|
|
135
152
|
plain `/mcp` uses the deployment's. It travels to the meta-tools and the
|
|
136
153
|
program as one boolean that drops writes from `CatalogService` search and
|
|
137
154
|
describe and makes `writeRefusal` (`src/tool-safety.ts`) refuse every write
|
|
138
155
|
call. Guides and skills are not filtered.
|
|
139
156
|
4. **Derive the registry view.** One `registry.scoped(...)` call with those
|
|
140
|
-
connector ids, exact `connector.tool` addresses when the
|
|
157
|
+
connector ids, exact `connector.tool` addresses when the pool declares a
|
|
141
158
|
narrower slice, and the subject and principal keys. Personal connectors use
|
|
142
159
|
the principal partition and result paging the subject partition with principal,
|
|
143
|
-
endpoint/pool, origin, connector, and tool bindings. Paging
|
|
144
|
-
|
|
160
|
+
endpoint/pool, origin, connector, and tool bindings. Paging re-admits the
|
|
161
|
+
caller at its endpoint and rechecks the endpoint's `readOnly`; no caller parameter selects a partition (`test/identity-scope.test.ts`).
|
|
145
162
|
5. **Serve.** Refuse `?toolkit=` with a 404 — the toolkits are gone
|
|
146
163
|
([#178](https://github.com/zackbart/connecta/issues/178)) but their URLs were
|
|
147
164
|
handed out, and retiring a scoping boundary into fail-open is worse than any
|
|
@@ -157,7 +174,7 @@ never select an identity, grant, connector view, or pool. Direct and program
|
|
|
157
174
|
activity records retain only the self-declared `clientName` and `clientVersion`,
|
|
158
175
|
validated by one ASCII grammar at the record builder, SQL write/read boundaries,
|
|
159
176
|
and authenticated activity read route. Names match
|
|
160
|
-
`[A-Za-z0-9][A-Za-z0-9 ._@/+-]{0,63}`; versions match
|
|
177
|
+
`[A-Za-z0-9@][A-Za-z0-9 ._@/+-]{0,63}`; versions match
|
|
161
178
|
`[A-Za-z0-9][A-Za-z0-9._+-]{0,31}`, each as a whole string. Names allow up to
|
|
162
179
|
64 characters, versions up to 32. `__proto__`, `constructor`, and `prototype`
|
|
163
180
|
are reserved. Invalid values are absent, never truncated or escaped. Capabilities and other client metadata never enter activity.
|
|
@@ -191,13 +208,14 @@ request-scoped sent-credential set and pass through `redactAgentOutput` before
|
|
|
191
208
|
serialization. The SDK's `server/discover` response and registered meta-tool
|
|
192
209
|
results use that same set; connector contexts retain only an opaque scope.
|
|
193
210
|
|
|
194
|
-
| Refusal
|
|
195
|
-
|
|
|
196
|
-
| Request capacity exhausted
|
|
197
|
-
| Server shutting down
|
|
198
|
-
| Request lifetime exceeded
|
|
199
|
-
| Pool missing or refused
|
|
200
|
-
| Origin or admitted access forbidden
|
|
211
|
+
| Refusal | HTTP status | Application code | Previous code |
|
|
212
|
+
| ------------------------------------ | ----------- | ---------------- | ------------- |
|
|
213
|
+
| Request capacity exhausted | 503 | `-33001` | `-31001` |
|
|
214
|
+
| Server shutting down | 503 | `-33002` | `-31002` |
|
|
215
|
+
| Request lifetime exceeded | 504 | `-33003` | none |
|
|
216
|
+
| Pool missing or refused | 404 | `-33004` | none |
|
|
217
|
+
| Origin or admitted access forbidden | 403 | `-33005` | none |
|
|
218
|
+
| Listen classification body too large | 413 | `-33006` | none |
|
|
201
219
|
|
|
202
220
|
These allocations are outside JSON-RPC's reserved `-32768..-32000` range and
|
|
203
221
|
MCP's `-32020..-32099` range. The prior admission codes were also outside those
|
|
@@ -273,6 +291,40 @@ cannot be faked as sequential leases: consuming a rolling token before a later
|
|
|
273
291
|
rule refuses would charge a call that never reached the provider, the exact
|
|
274
292
|
accounting error a budget exists to prevent. Both layers are pinned by
|
|
275
293
|
`test/request-admission.test.ts` and `test/call-admission.test.ts`.
|
|
294
|
+
Every defined budget must be an object with `kind: "rolling-window"` and positive
|
|
295
|
+
whole `maxCalls` and `windowMs` values. Malformed budgets, including `false`,
|
|
296
|
+
`null`, `0`, and `""`, throw during construction, even when the rule also sets
|
|
297
|
+
`maxConcurrency`.
|
|
298
|
+
|
|
299
|
+
An admitted tool call can charge sequential auxiliary downstream requests with the internal
|
|
300
|
+
`chargeConnectorCall(ctx)` hook in `src/call-admission-context.ts`. Invocation
|
|
301
|
+
binds the context to its existing permit; the hook returns a Promise over an
|
|
302
|
+
internal Effect program. The charge uses the enclosing call's partition
|
|
303
|
+
and concurrency slot, so a connector with `maxConcurrency: 1` can perform a
|
|
304
|
+
lookup without queuing behind itself. Exhaustion returns the same
|
|
305
|
+
`CallAdmissionError` and retry delay as ordinary call admission, before the
|
|
306
|
+
auxiliary request is sent. Charges against a configured policy also refuse
|
|
307
|
+
after cancellation, permit release, or controller shutdown. The hook adds no
|
|
308
|
+
public connector option or context member. Contexts outside registry invocation
|
|
309
|
+
have no bound permit, so the hook does nothing there. Providers await the hook
|
|
310
|
+
before sending and propagate its refusal; it does not acquire concurrency for
|
|
311
|
+
parallel auxiliary requests.
|
|
312
|
+
|
|
313
|
+
Cloudflare's REST connector charges each uncached zone-ownership lookup used
|
|
314
|
+
to enforce an account pin, in addition to the enclosing tool-call attempt.
|
|
315
|
+
Explicitly pinned zones and cached ownership need no extra request or charge.
|
|
316
|
+
Its limits of three ownership lookups per call and 60 per five minutes per
|
|
317
|
+
connector remain: they bound pin checks separately from the connector's larger
|
|
318
|
+
shared call budget. The Cloudflare provider tests cover these checks through
|
|
319
|
+
registry invocation as well as the existing pin refusals.
|
|
320
|
+
|
|
321
|
+
Only an omitted or `undefined` `callAdmission` selects a connector's default.
|
|
322
|
+
The shared schema rejects malformed defined policies at factory construction,
|
|
323
|
+
and the registry validates custom connector policies before registration.
|
|
324
|
+
Falsy values such as `false`, `null`, `0`, and `""` are construction errors
|
|
325
|
+
(INV-11), never a way to disable admission. The generated provider fixtures
|
|
326
|
+
exercise these refusals and unchanged defaults in
|
|
327
|
+
`test/call-admission-options.test.ts`.
|
|
276
328
|
|
|
277
329
|
### Production Worker disconnects
|
|
278
330
|
|
|
@@ -335,6 +387,173 @@ and deletes the Worker in `finally`. It uses no `waitUntil`, which would change
|
|
|
335
387
|
the lifetime being measured. The production runtime build number was not
|
|
336
388
|
exposed by the deployment; these findings are tied to the date and flags above.
|
|
337
389
|
|
|
390
|
+
### Production Access bypass for agent endpoints
|
|
391
|
+
|
|
392
|
+
P0 of [#837](https://github.com/zackbart/connecta/issues/837) asked whether
|
|
393
|
+
`/agent/*` can skip a Worker-level Cloudflare Access application, so an agent
|
|
394
|
+
sends only its `cta_` token, while every other path keeps Access and
|
|
395
|
+
`ctx.access`. It can. The probe ran on 2026-10-15 against a disposable Worker on
|
|
396
|
+
its `workers.dev` hostname, with compatibility date `2026-09-01`, `cf`
|
|
397
|
+
1.0.0-beta.14, Wrangler 4.114.0, and curl 8.7.1 with `--path-as-is`, so raw
|
|
398
|
+
paths reached the edge unnormalized. The Worker-level application allowed only
|
|
399
|
+
a probe service token. Its unauthenticated refusal was therefore Access's 403
|
|
400
|
+
page rather than a login redirect. The [observations](https://github.com/zackbart/connecta/blob/main/scripts/probes/access-agent-bypass-2026-10-15.json)
|
|
401
|
+
record three identical rounds after each configuration change had produced
|
|
402
|
+
three consecutive matching responses.
|
|
403
|
+
|
|
404
|
+
- **Precedence.** A self-hosted application whose destination is
|
|
405
|
+
`{ "type": "public", "uri": "<host>/agent/*" }`, with one `bypass` policy
|
|
406
|
+
including `everyone`, overrides the Worker-level application on matching
|
|
407
|
+
paths only. Unauthenticated requests reach the Worker there. `/mcp`, `/`,
|
|
408
|
+
and every unmatched path still get the Access refusal. A service token is
|
|
409
|
+
still admitted on them with `ctx.access` from the Worker-level application.
|
|
410
|
+
This confirms Cloudflare's documented hierarchy, and that Worker-level
|
|
411
|
+
protection still covers the `workers.dev` hostname.
|
|
412
|
+
- **`ctx.access` on a bypassed request is present, not absent.** Its `aud` is
|
|
413
|
+
nonempty and is the _path_ application's AUD, not the Worker-level one.
|
|
414
|
+
`getIdentity()` resolved to `undefined` for every credential tested: none, a
|
|
415
|
+
valid service token, a `cta_`-style bearer token, and a forged
|
|
416
|
+
`Cf-Access-Jwt-Assertion`. A service token on a protected path also gave
|
|
417
|
+
`undefined` identity, carrying the Worker-level AUD. A human
|
|
418
|
+
`CF_Authorization` cookie on a bypassed path was not tested, so what
|
|
419
|
+
`getIdentity()` returns there is unknown. That is one more reason the agent
|
|
420
|
+
route ignores Access context rather than relying on its contents.
|
|
421
|
+
- **API shape.** A Worker destination is exclusive to one application. A
|
|
422
|
+
second application with `{ "type": "worker", "worker_id" }` plus `uri` or
|
|
423
|
+
`path` was refused with 409 `destination belongs to another application`. The
|
|
424
|
+
bypass must be hostname-and-path, stored with domain `<host>/agent/*`, and it
|
|
425
|
+
covers only that exact hostname.
|
|
426
|
+
- **Propagation.** Creating or deleting each application took 15–19 seconds to
|
|
427
|
+
settle across runs. While a deployment or application change was propagating,
|
|
428
|
+
the edge returned transient 500 (`error code: 1104`) and 404 (`error code:
|
|
429
|
+
1042` or `1043`) responses. The recorded run has one such 404, on the
|
|
430
|
+
Worker-level-only service-token control for `/agent/x`. Deleting the path
|
|
431
|
+
application restored the 403 on `/agent/x`.
|
|
432
|
+
|
|
433
|
+
| Raw request path | Access outcome | Worker pathname (+ search) |
|
|
434
|
+
| -------------------------- | -------------- | -------------------------- |
|
|
435
|
+
| `/agent/x` | bypassed | `/agent/x` |
|
|
436
|
+
| `/agent/assistant-alice` | bypassed | `/agent/assistant-alice` |
|
|
437
|
+
| `/agent/` | bypassed | `/agent/` |
|
|
438
|
+
| `/agent/x/` | bypassed | `/agent/x/` |
|
|
439
|
+
| `/agent/x?y` | bypassed | `/agent/x?y` |
|
|
440
|
+
| `/AGENT/x` | bypassed | `/AGENT/x` |
|
|
441
|
+
| `/%61gent/x` | bypassed | `/%61gent/x` |
|
|
442
|
+
| `//agent/x` | bypassed | `//agent/x` |
|
|
443
|
+
| `/agent/%2f/mcp` | bypassed | `/agent/%2f/mcp` |
|
|
444
|
+
| `/agent/x%2f..%2f..%2fmcp` | bypassed | `/agent/x%2f..%2f..%2fmcp` |
|
|
445
|
+
| `/mcp/../agent/x` | bypassed | `/agent/x` |
|
|
446
|
+
| `/mcp` | protected | not reached |
|
|
447
|
+
| `/` | protected | not reached |
|
|
448
|
+
| `/agent` | protected | not reached |
|
|
449
|
+
| `/agentx` | protected | not reached |
|
|
450
|
+
| `/mcp/agent/x` | protected | not reached |
|
|
451
|
+
| `/agent/../mcp` | protected | not reached |
|
|
452
|
+
| `/agent/x/../../mcp` | protected | not reached |
|
|
453
|
+
| `/agent/%2e%2e/mcp` | protected | not reached |
|
|
454
|
+
| `/agent/%2E%2E/mcp` | protected | not reached |
|
|
455
|
+
| `/agent%2f..%2fmcp` | protected | not reached |
|
|
456
|
+
|
|
457
|
+
Access matches after resolving dot segments, including `%2e` forms. It matches
|
|
458
|
+
case-insensitively, decodes unreserved escapes such as `%61`, collapses a
|
|
459
|
+
leading `//`, and treats `%2f` as data, not a separator. The effective Worker
|
|
460
|
+
pathname, `new URL(request.url).pathname`, is the raw request path with case,
|
|
461
|
+
escapes, and slashes untouched, except that dot segments arrive resolved:
|
|
462
|
+
`/mcp/../agent/x` arrives as `/agent/x`. Every path that
|
|
463
|
+
resolves outside `/agent/*`, including every `/mcp` form, stayed protected. The
|
|
464
|
+
inverse is the risk: Access bypasses paths
|
|
465
|
+
that connecta must not treat as an agent pool. Connecta's router compares raw
|
|
466
|
+
pathnames without decoding, case folding, or slash collapsing, so an `/agent/`
|
|
467
|
+
route must keep that property. It should accept exactly
|
|
468
|
+
`/agent/<pool>`, with `<pool>` byte-equal to a declared pool name. It should
|
|
469
|
+
refuse an empty segment, a trailing slash, any extra segment, `%`, uppercase
|
|
470
|
+
variants, and a leading `//`, all with the same 404. It must ignore
|
|
471
|
+
`ctx.access` entirely, since a bypassed request still carries an AUD.
|
|
472
|
+
[Agent endpoints](./auth.md#agent-endpoints) implement exactly this in
|
|
473
|
+
`src/routes/agent-path.ts`, and `test/agent-endpoints.test.ts` replays every
|
|
474
|
+
row of the table above. The hyphenated row stands for any two-part pool name
|
|
475
|
+
such as `<agent>-<user>`. The pool names in the recorded paths were replaced
|
|
476
|
+
with placeholders after the run; the edge behavior for that path shape was
|
|
477
|
+
unaffected.
|
|
478
|
+
|
|
479
|
+
To configure the bypass, keep the Worker-level application described in
|
|
480
|
+
[Cloudflare Access on Workers](./auth.md#cloudflare-access-on-workers). Then,
|
|
481
|
+
for each hostname agents use (the `workers.dev` hostname, each Custom Domain,
|
|
482
|
+
or route host), add a self-hosted Access application. In the dashboard, set
|
|
483
|
+
domain to that hostname and path to `agent/*`, with one policy whose action is
|
|
484
|
+
**Bypass** and whose include rule is **Everyone**. Through the API, that is:
|
|
485
|
+
|
|
486
|
+
```json
|
|
487
|
+
{
|
|
488
|
+
"type": "self_hosted",
|
|
489
|
+
"name": "connecta agents",
|
|
490
|
+
"destinations": [{ "type": "public", "uri": "connecta.example.com/agent/*" }],
|
|
491
|
+
"policies": [{ "name": "agents bypass", "decision": "bypass", "include": [{ "everyone": {} }] }]
|
|
492
|
+
}
|
|
493
|
+
```
|
|
494
|
+
|
|
495
|
+
Only the `workers.dev` hostname was probed. A Custom Domain application is the
|
|
496
|
+
same documented hostname-and-path form, but it was not observed here. Do not
|
|
497
|
+
rely on a fixed wait after a change. Changes settled in 15–19 seconds, yet one
|
|
498
|
+
earlier round still saw a 403 on a bypassed path more than 30 seconds after
|
|
499
|
+
the change. Test only after the positive and negative controls agree together
|
|
500
|
+
for several consecutive rounds. The positive controls are bypassed paths
|
|
501
|
+
reaching the Worker with the path AUD and a service token reaching a protected
|
|
502
|
+
path with the Worker-level AUD; the negative controls are `/mcp` and `/`
|
|
503
|
+
returning the Access refusal. The current script requires five such rounds
|
|
504
|
+
after creating the bypass and three after other changes. The recorded run
|
|
505
|
+
predates this: after the bypass it converged only the positive `/agent/x`
|
|
506
|
+
control, then waited a fixed 90 seconds, and its negative controls were checked
|
|
507
|
+
in the recorded rounds rather than before them.
|
|
508
|
+
|
|
509
|
+
To repeat from a repository checkout, run
|
|
510
|
+
`node scripts/probes/access-agent-bypass.mjs --profile <name> [output.json]`.
|
|
511
|
+
`--profile` is required and has no default; every `cf` call passes it
|
|
512
|
+
explicitly. Name the profile for the account that should host the probe; it
|
|
513
|
+
needs
|
|
514
|
+
Access application, policy, and service-token writes, plus Workers script
|
|
515
|
+
writes. The script deploys and deletes the Worker with `cf`. `cf deploy` builds
|
|
516
|
+
through Wrangler 4.136.0 or newer, so the probe installs Wrangler 4.145.0 into
|
|
517
|
+
its temporary project. Wrangler needs no login. The script records only the
|
|
518
|
+
count of existing Access applications, then names everything it creates
|
|
519
|
+
`connecta-probe-<run>`. It force-deletes and verifies each resource in
|
|
520
|
+
`finally`, then checks that the non-probe application ids are unchanged.
|
|
521
|
+
Observations record statuses, outcome classes, pathnames, and which
|
|
522
|
+
application's AUD matched, compared by digest inside the Worker. They contain no
|
|
523
|
+
hostnames, account ids, tokens, cookies, or JWTs. `cf` delete commands prompt
|
|
524
|
+
for confirmation and exit 0 without deleting when stdin is not a terminal, so
|
|
525
|
+
the script passes `--force`. `cf deploy` printed `Deploy complete` and then
|
|
526
|
+
kept running, so the script stops it after that line. The recorded run deployed
|
|
527
|
+
with Wrangler 4.114.0 and used `cf` for Access. A standalone deploy and delete
|
|
528
|
+
verified the `cf` deploy and delete path. The full probe has not been rerun on
|
|
529
|
+
that path.
|
|
530
|
+
|
|
531
|
+
Every subprocess (`cf`, `curl`, `npm`) runs through one runner. It enforces a
|
|
532
|
+
deadline by killing the process group, keeps stdout and stderr in memory, and
|
|
533
|
+
returns only facts: success, exit code, timeout, HTTP status, numeric API
|
|
534
|
+
code, and an error slug from a fixed set (anything else is `unknown`). Commands
|
|
535
|
+
that return data are parsed inside the runner against schemas that copy only
|
|
536
|
+
whitelisted fields: ids, names, counts, booleans, and the Worker's `ctx.access`
|
|
537
|
+
summary. Credentials stay in a holder that prints as `[secret]`. An unexpected
|
|
538
|
+
shape, including `null` where a list belongs, is a failure, never an empty
|
|
539
|
+
result. Errors are fixed diagnostics plus those facts. Teardown deletes every
|
|
540
|
+
tracked id on its own. Then Access applications, service tokens, and Workers
|
|
541
|
+
each get their own bounded discovery-and-delete pass by name, for resources
|
|
542
|
+
whose create response was lost, so a failure in one family never skips
|
|
543
|
+
another. Each discovered resource is deleted independently. Access policies are
|
|
544
|
+
inline on their applications and go with them. A resource counts as gone only
|
|
545
|
+
when a successful lookup omits it, and the final inventory verifies each family
|
|
546
|
+
separately. Run
|
|
547
|
+
`node scripts/probes/access-agent-bypass.mjs --self-test` to check the runner
|
|
548
|
+
offline. It feeds canary secrets, suffixed slugs, malformed JSON, and a stalled
|
|
549
|
+
process tree through it. It also runs teardown against a fake `cf` whose
|
|
550
|
+
application listing always fails, after a lost token-create response, and
|
|
551
|
+
checks that the token is still found and deleted. It also checks that a Worker
|
|
552
|
+
that refuses deletion does not block another Worker's deletion. It runs no real
|
|
553
|
+
`cf`, `curl`, or network command. This
|
|
554
|
+
hardening and the joint convergence are verified offline only and have not run
|
|
555
|
+
against Cloudflare.
|
|
556
|
+
|
|
338
557
|
## Storage, credentials, and connectors
|
|
339
558
|
|
|
340
559
|
A deployment has one store. On Workers it is one D1 database
|
|
@@ -374,8 +593,8 @@ storage clock. Supply at most one option. Custom adapters must implement both;
|
|
|
374
593
|
a delayed absolute-expiry write must not restart a relative TTL at commit time. Each SQL claim
|
|
375
594
|
is one statement, which SQLite executes atomically and D1 serializes on its
|
|
376
595
|
primary. Everything that must claim a key exactly once relies on it with no
|
|
377
|
-
read-then-write fallback:
|
|
378
|
-
|
|
596
|
+
read-then-write fallback: agent-token slot swaps and
|
|
597
|
+
adoption, OAuth handoff ownership, single-use connect links, every downstream
|
|
379
598
|
OAuth grant write and consent claim, and the result stash.
|
|
380
599
|
|
|
381
600
|
Every key is built in `src/storage/keys.ts`, which lists each family with its
|
|
@@ -544,9 +763,9 @@ configuration, never runtime registration. `authScope: "shared" | "personal"`
|
|
|
544
763
|
partitions connecta-owned context — state, credentials, OAuth, catalogs, observed
|
|
545
764
|
shapes — by principal, and _only_ connecta-owned context: a secret a custom
|
|
546
765
|
handler closes over is shared JavaScript state, and `remoteMcp()` refuses the
|
|
547
|
-
literal-headers-plus-personal version of that mistake. Visibility
|
|
548
|
-
|
|
549
|
-
|
|
766
|
+
literal-headers-plus-personal version of that mistake. Visibility (maintainers
|
|
767
|
+
and pool `tools`) is a separate rule; hiding a connector does not change who
|
|
768
|
+
owns its auth.
|
|
550
769
|
|
|
551
770
|
### Errors and records
|
|
552
771
|
|
|
@@ -751,7 +970,7 @@ manual review evidence instead of claiming a machine-checkable JSON pin.
|
|
|
751
970
|
|
|
752
971
|
The generic REST connector (`src/providers/_shared/rest/`) serves
|
|
753
972
|
`<vendor>_api_search`, `_api_details`, `_api_read`, and `_api_write` over an
|
|
754
|
-
`OperationIndex` that `
|
|
973
|
+
`OperationIndex` that `pnpm run providers:spec` compiles offline from the
|
|
755
974
|
provider's pinned `openapi.source.json` into `openapi.generated.ts`.
|
|
756
975
|
Each call is matched to an operation and checked against its parameters before
|
|
757
976
|
transport; `_api_read` admits GET and the vendor's reviewed read-only POSTs in
|
|
@@ -827,8 +1046,90 @@ every vendor ships one reviewed table through one mechanism
|
|
|
827
1046
|
labelled or typed secret records lose their value, environment containers
|
|
828
1047
|
lose their values, header, cookie, and query rules lose theirs, external
|
|
829
1048
|
destinations keep their origin, and every URL loses userinfo and
|
|
830
|
-
credential-named query parameters or fragments
|
|
831
|
-
shared; each vendor states its own.
|
|
1049
|
+
credential-named query parameters or fragments under its URL policy.
|
|
1050
|
+
Exemptions are never shared; each vendor states its own. Every REST vendor
|
|
1051
|
+
reviews the pinned spec's free-text fields for embedded vendor-minted links
|
|
1052
|
+
and records its verdict and source limitations beside its value-safety table.
|
|
1053
|
+
Stripe, Vercel, and Cloudflare use **reviewed paths plus global signing
|
|
1054
|
+
parameters**. Literal AWS SigV4/Google V4 `X-Amz-`/`X-Goog-` Signature,
|
|
1055
|
+
Credential and Security-Token values are withheld across all decoded string
|
|
1056
|
+
values. Generic credential names (`code`, `key`, `token`, `signature`, `sig`,
|
|
1057
|
+
etc.) and credential-looking fragments apply only to reviewed diagnostic
|
|
1058
|
+
paths and vendor error text. Matching destinations lose their entire query
|
|
1059
|
+
and fragment. The tables list exact operations/resource paths with evidence:
|
|
1060
|
+
Stripe charge failure/outcome messages, Vercel deployment failures and
|
|
1061
|
+
build/runtime log text, Cloudflare Workers/Pages build logs and telemetry
|
|
1062
|
+
message/error and raw string/object source payloads in both events and
|
|
1063
|
+
invocation maps. The shared review harness rejects embedded-URL operation
|
|
1064
|
+
keys absent from each vendor's pinned index and invalid path syntax.
|
|
1065
|
+
Vercel's unavailable full pin remains a review limitation.
|
|
1066
|
+
Generic parameters in descriptions, metadata, nicknames, billing footers,
|
|
1067
|
+
notes, DNS TXT, KV values and operator HTML retain their original bytes,
|
|
1068
|
+
including whole URL strings. Ordinary code/key URLs can be false positives
|
|
1069
|
+
in reviewed diagnostics. Existing structured-field/resource rules, reviewed
|
|
1070
|
+
`url:` per-parameter rules, and payer-facing exceptions still apply.
|
|
1071
|
+
Scans read the original escaped text before URL parsing or serialization;
|
|
1072
|
+
adjacent HTML or Markdown is text even without whitespace. Relative Markdown
|
|
1073
|
+
destinations are checked at their own query/fragment, without re-anchoring
|
|
1074
|
+
a nested proxy URL. Ordinary proxy parameters retain their bytes.
|
|
1075
|
+
Scans precede success/select/named projections and error mapping. Named
|
|
1076
|
+
Vercel streams use the same paths before clamping. Binary responses encoded
|
|
1077
|
+
as base64, including Cloudflare Worker JavaScript served as
|
|
1078
|
+
`application/javascript`, are returned unchanged, without decoding/scanning.
|
|
1079
|
+
Cloudflare omits an envelope cursor that URL sanitizing would change;
|
|
1080
|
+
ordinary cursors retain their bytes. No vendor-wide host/bucket policy is
|
|
1081
|
+
inferred. Each string is scanned with bounded linear passes over disjoint
|
|
1082
|
+
destinations, including large inputs.
|
|
1083
|
+
These three vendors enforce write admission with the shared
|
|
1084
|
+
`carriesWithheldLink(value)` scanner and `refuseWithheldLinks`/
|
|
1085
|
+
`refuseWithheldUpload` helpers, plus `refuseWithheldBase64` for documented
|
|
1086
|
+
base64 JSON fields. Every supported write transport (JSON,
|
|
1087
|
+
form-encoded, raw/base64 bytes, multipart parts and named writes) refuses
|
|
1088
|
+
with `invalid_args` and zero dispatch when a URL destination's own query
|
|
1089
|
+
or fragment contains the exact `[redacted]` placeholder, plain or up to four
|
|
1090
|
+
layers encoded. Upload admission reads decoded bytes and each multipart
|
|
1091
|
+
string/blob. Vercel deployment `files[].data` with `encoding: "base64"`
|
|
1092
|
+
and Cloudflare KV bulk `value` with `base64: true` pass through the same
|
|
1093
|
+
byte inspection with one decode, bounded by the field's own encoded length
|
|
1094
|
+
(decoded bytes are at most three quarters of encoded characters). Cloudflare
|
|
1095
|
+
KV additionally enforces its documented 25 MiB decoded value limit;
|
|
1096
|
+
malformed base64 and values beyond that limit refuse with `invalid_args`.
|
|
1097
|
+
The host's inbound request cap still applies (Node `maxBodyBytes`, or the
|
|
1098
|
+
Workers platform); response and raw-upload limits do not bound JSON fields.
|
|
1099
|
+
JSON admission iteratively checks every key and value, with no separate
|
|
1100
|
+
depth or node-count ceiling below the accepted request's size. Cyclic
|
|
1101
|
+
direct arguments refuse with `invalid_args`; repeated non-cyclic references
|
|
1102
|
+
are inspected. The scans remain linear in the supplied and decoded bytes.
|
|
1103
|
+
Withheld values must not be written back; obtain the original
|
|
1104
|
+
link or omit the field. A placeholder outside a destination is ordinary
|
|
1105
|
+
text. Notion keeps its stricter reviewed-host admission and proxy policy.
|
|
1106
|
+
Vendors whose responses embed the file links they mint can
|
|
1107
|
+
use `withholdSignedLinks(text, hosts)` or
|
|
1108
|
+
`withholdSignedLinksIn(value, hosts)` with a reviewed `FileHosts` policy
|
|
1109
|
+
(`domains` and S3 `buckets`). Notion applies the latter in its `result`
|
|
1110
|
+
hook and before mapping errors, so Markdown, async task results, paging,
|
|
1111
|
+
named-tool results, and `select` all pass through it. The linear scan
|
|
1112
|
+
matches only the current URL/destination's actual authority: a reviewed
|
|
1113
|
+
domain or subdomain, or a reviewed S3 bucket, virtual-hosted or exactly
|
|
1114
|
+
the first path segment on an S3 endpoint. A secondary scheme or `//` in
|
|
1115
|
+
another URL's path or query cannot re-anchor it; those proxy links keep
|
|
1116
|
+
their bytes. Schemes and slashes may be escaped. File links lose their
|
|
1117
|
+
whole query and fragment from a literal, percent-, entity-, or
|
|
1118
|
+
`\u`-encoded opener through the next whitespace, quote, or angle bracket;
|
|
1119
|
+
trailing closing punctuation comes back. AWS SigV4 and Google V4
|
|
1120
|
+
`Signature`, `Credential`, and `Security-Token` parameter values are
|
|
1121
|
+
withheld wherever their literal signing names occur, at any host, with
|
|
1122
|
+
plain or encoded equals signs. `carriesWithheldLink(value, hosts)` is
|
|
1123
|
+
the shared write-admission helper: Notion refuses a write before dispatch
|
|
1124
|
+
when a file link's own query/fragment carries the exact `[redacted]`
|
|
1125
|
+
placeholder, plain or up to four layers encoded. Admission resumes at
|
|
1126
|
+
each actual Markdown destination's close, including adjacent links and
|
|
1127
|
+
images and balanced/escaped parentheses; leading spaces, tabs, and one
|
|
1128
|
+
line ending before the destination are recognized. A marker after the close is
|
|
1129
|
+
ordinary text. This boundary is independent of redaction's fail-closed
|
|
1130
|
+
span. Each string is scanned alone; split rich-text runs, encoded hosts
|
|
1131
|
+
or signing names, bare hosts without `//`, and userinfo before a reviewed
|
|
1132
|
+
host are outside this scan's threat model.
|
|
832
1133
|
4. **Errors.** A reviewed operation, or an unreviewed one whose request
|
|
833
1134
|
accepts a credential-named field, answers failures with the vendor's codes
|
|
834
1135
|
and status but not its text, unless its verdict is wrapped in
|
|
@@ -908,7 +1209,7 @@ regardless of wrapping.
|
|
|
908
1209
|
|
|
909
1210
|
`createConnecta` takes closed typed `ui`, `vault`, and `activity`
|
|
910
1211
|
slots, with factories at `/ui`, `/credentials`, and `/activity`
|
|
911
|
-
and
|
|
1212
|
+
and agent tokens at `/auth/access-tokens`. Root exports the contracts, never the implementations, and there
|
|
912
1213
|
is no module array, runtime registration, or plugin lifecycle. Core keeps
|
|
913
1214
|
discovery, the executor contract, invocation, permissions, and OAuth callback
|
|
914
1215
|
verification; an omitted module contributes no runtime work at all.
|
|
@@ -935,10 +1236,13 @@ connecta shows a person — the operator shell, the OAuth callback, a browser's
|
|
|
935
1236
|
404 — reads it. `src/operator-ui/tokens.css`
|
|
936
1237
|
resolves every color, radius, and font through a custom property and mixes the
|
|
937
1238
|
rest from those with `color-mix`; `page.css` holds the base typography and the
|
|
938
|
-
|
|
939
|
-
page). The UI's `browser.css` imports both,
|
|
940
|
-
|
|
1239
|
+
pieces server-rendered pages share (top bar, buttons, badges, messages, the
|
|
1240
|
+
one-message status page). The UI's `browser.css` imports both, then one file
|
|
1241
|
+
per area under `src/operator-ui/styles/` (fonts, shared components, shell, and
|
|
1242
|
+
each page). The build writes the shared CSS, minified, into
|
|
1243
|
+
`src/page-styles.ts`, with the hashed font paths, a module that core imports
|
|
941
1244
|
without the UI bundle, so an OAuth callback still renders with the UI omitted.
|
|
1245
|
+
Those pages link the fonts only when the UI module serves them.
|
|
942
1246
|
`renderPage` in `src/branding.ts` is the one layout: head, favicon (the default
|
|
943
1247
|
is linked only when the UI serves it), `data-scheme`, the tokens with the theme
|
|
944
1248
|
after them, and the masthead. So `branding.theme` only has to append a `:root`
|
|
@@ -952,13 +1256,18 @@ Maintained providers act as the caller downstream. `src/connector-caller.ts`
|
|
|
952
1256
|
attaches the admitted identity beside the request-scoped context, readable only
|
|
953
1257
|
by in-repo code and set from authorization, never arguments. Google Workspace
|
|
954
1258
|
domain-wide delegation
|
|
955
|
-
(`src/providers/google/`, first consumed by `./providers/gmail`) hands the
|
|
1259
|
+
(`src/providers/_shared/google/`, first consumed by `./providers/gmail`) hands the
|
|
956
1260
|
admitted identity to a deployment-config `subject` function and mints a
|
|
957
1261
|
service-account token as whatever Workspace address it returns. The caller
|
|
958
1262
|
carries whether an inbound provider _authenticated_ it: an open deployment
|
|
959
1263
|
admits everyone as the anonymous actor, and the function is never asked about
|
|
960
|
-
them.
|
|
961
|
-
|
|
1264
|
+
them. It also carries the admitted pool's `owner`, which core copies from
|
|
1265
|
+
config: an agent acts as it, before and instead of the function. In a pool
|
|
1266
|
+
without one, the scoped registry view omits a mapping-subject connector for an
|
|
1267
|
+
agent (`src/registry.ts`, keyed on the tag in `src/workspace-subject.ts`), as it
|
|
1268
|
+
omits a personal connector, and the provider still fails closed if reached. A fixed `subject` address is every
|
|
1269
|
+
caller's. No caller, an unauthenticated one, or no address fails
|
|
1270
|
+
`auth_required` before any request leaves. The provider never chooses the account and a
|
|
962
1271
|
deployment's own connector still cannot read the caller (PRINCIPLES.md, INV-3; `test/google-workspace-delegation.test.ts`). Its tokens live in a
|
|
963
1272
|
bounded module-level map, in memory only, keyed by the key's own digest as well
|
|
964
1273
|
as the account, subject, and scopes. A mint in flight is shared across requests
|
|
@@ -1017,7 +1326,7 @@ Effect callers skip the Promise and take the program: `admit` and
|
|
|
1017
1326
|
and `acquireCallScoped` for call admission, `closeScope` and `closeScopeOnExit`
|
|
1018
1327
|
for a connector scope, `withDeadlineEffect` where Promise code has
|
|
1019
1328
|
`withDeadline`. The build prunes every declaration no `exports` target reaches,
|
|
1020
|
-
and `
|
|
1329
|
+
and `pnpm run check:declarations` fails if what remains names an Effect type.
|
|
1021
1330
|
|
|
1022
1331
|
### One runner
|
|
1023
1332
|
|
|
@@ -1152,7 +1461,7 @@ resolve a single copy. Adopting an unstable subpath is its own pull request.
|
|
|
1152
1461
|
|
|
1153
1462
|
The stable `4.0.0` release was re-evaluated on 2026-10-01 with
|
|
1154
1463
|
[`scripts/probes/effect-v4-evaluation.mjs`](https://github.com/zackbart/connecta/blob/main/scripts/probes/effect-v4-evaluation.mjs).
|
|
1155
|
-
Run `
|
|
1464
|
+
Run `pnpm run build`, then `node scripts/probes/effect-v4-evaluation.mjs output.json`.
|
|
1156
1465
|
The [raw observations](https://github.com/zackbart/connecta/blob/main/scripts/probes/effect-v4-evaluation-2026-10-01.json)
|
|
1157
1466
|
are synthetic Node requests and neutral esbuild bundles, not live-client or
|
|
1158
1467
|
workerd lifetime verification. Bundle figures add representative prototypes
|
|
@@ -1190,7 +1499,7 @@ replacement's benefit or compatibility.
|
|
|
1190
1499
|
The core's cost is recorded rather than guessed. Against 0.24.4 the root entry
|
|
1191
1500
|
grew from 235,346 to 279,526 bytes gzip, the Worker example from 263,949 to
|
|
1192
1501
|
314,336, and each provider by about 36 KB, since each reaches Effect through
|
|
1193
|
-
`remoteMcp()`; `
|
|
1502
|
+
`remoteMcp()`; `pnpm run check:bundle` caps every entry against
|
|
1194
1503
|
`scripts/bundle-budget.json`. An install carries about 53 MB of `effect`, with
|
|
1195
1504
|
no dependencies of its own.
|
|
1196
1505
|
|
|
@@ -1206,7 +1515,7 @@ src/
|
|
|
1206
1515
|
catalog-drift.ts reviewed classifications and the drift counts a refresh produces
|
|
1207
1516
|
provider.ts defineProvider(): the one maintained-provider shape
|
|
1208
1517
|
activity.ts optional history factory and best-effort recorder
|
|
1209
|
-
auth/
|
|
1518
|
+
auth/ Cloudflare Access, clerk (optional peer), downstream OAuth (remote and static)
|
|
1210
1519
|
executors/ the QuickJS pool and child (Node only)
|
|
1211
1520
|
node.ts listen() (Node only)
|
|
1212
1521
|
d1.ts, sqlite.ts the two storage drivers; storage/ holds the SQL core and key families
|
|
@@ -1214,7 +1523,7 @@ src/
|
|
|
1214
1523
|
|
|
1215
1524
|
There are exactly two deployment shapes — `templates/node/`, which
|
|
1216
1525
|
`connecta init` copies with its container files, and `examples/worker/` — and
|
|
1217
|
-
`test/deployment-shapes.node.test.ts` with `
|
|
1526
|
+
`test/deployment-shapes.node.test.ts` with `pnpm run check:examples` keeps both
|
|
1218
1527
|
compiling and configuring the real thing.
|
|
1219
1528
|
|
|
1220
1529
|
## Sharp edges
|
|
@@ -1232,7 +1541,8 @@ compiling and configuring the real thing.
|
|
|
1232
1541
|
SIGTERM/SIGINT.
|
|
1233
1542
|
- **Structural mistakes throw at construction.** A duplicate connector id, an
|
|
1234
1543
|
invalid admission rule or limit, an unknown option at any depth, the old
|
|
1235
|
-
boolean `accessTokens` option,
|
|
1544
|
+
boolean `accessTokens` option, malformed or repeated `agents` or agents
|
|
1545
|
+
without `accessTokens`, a missing executor, or an executor without a
|
|
1236
1546
|
lifecycle brand:
|
|
1237
1547
|
all refuse to boot (`test/config.test.ts`, `test/registry.test.ts`). Starting
|
|
1238
1548
|
in the wrong shape is worse than not starting.
|