@zackbart/connecta 0.20.0 → 0.21.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.
Files changed (45) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/README.md +3 -1
  3. package/bin/connecta.mjs +23 -6
  4. package/dist/auth/bearer.d.ts +2 -2
  5. package/dist/auth/bearer.js +2 -2
  6. package/dist/auth/clerk.js +1 -0
  7. package/dist/auth/cloudflare-access.d.ts +8 -0
  8. package/dist/auth/cloudflare-access.js +66 -0
  9. package/dist/execute.js +12 -6
  10. package/dist/index.d.ts +2 -2
  11. package/dist/index.js +2 -2
  12. package/dist/meta-tools.js +3 -3
  13. package/dist/operator-ui/generated.js +1 -1
  14. package/dist/operator-ui/model.d.ts +3 -3
  15. package/dist/operator-ui/view.d.ts +1 -1
  16. package/dist/operator-ui/view.js +6 -3
  17. package/dist/routes/access-tokens.d.ts +1 -1
  18. package/dist/routes/access-tokens.js +2 -2
  19. package/dist/routes/activity.js +2 -2
  20. package/dist/routes/credentials.js +1 -1
  21. package/dist/routes/mcp.js +1 -1
  22. package/dist/routes/oauth.js +1 -1
  23. package/dist/routes/shared.d.ts +4 -4
  24. package/dist/routes/shared.js +10 -10
  25. package/dist/routes/ui.js +12 -9
  26. package/dist/skills.d.ts +1 -1
  27. package/dist/skills.js +7 -4
  28. package/dist/types.d.ts +37 -22
  29. package/dist/ui.d.ts +1 -1
  30. package/dist/ui.js +3 -3
  31. package/dist/version.d.ts +1 -1
  32. package/dist/version.js +1 -1
  33. package/documentation/architecture.md +7 -4
  34. package/documentation/auth.md +71 -7
  35. package/documentation/code-mode.md +4 -4
  36. package/documentation/meta-tools.md +22 -20
  37. package/documentation/operations.md +23 -8
  38. package/documentation/operator-ui.md +21 -5
  39. package/documentation/upgrading.md +76 -4
  40. package/ethos.md +2 -3
  41. package/examples/worker/README.md +52 -32
  42. package/examples/worker/src/index.ts +32 -38
  43. package/examples/worker/wrangler.jsonc +12 -4
  44. package/package.json +5 -1
  45. package/templates/node/package.json +1 -1
@@ -45,6 +45,10 @@ one.
45
45
  npx @zackbart/connecta init my-deployment
46
46
  cd my-deployment && npm install && npm start
47
47
  CONNECTA_TOKEN=… npx connecta doctor --url http://localhost:8787
48
+
49
+ # A Worker protected by Cloudflare Access
50
+ CF_ACCESS_CLIENT_ID=… CF_ACCESS_CLIENT_SECRET=… \
51
+ npx connecta doctor --url https://connecta.example.workers.dev
48
52
  ```
49
53
 
50
54
  `init` copies the template, pins the generated deployment to the CLI package's
@@ -59,12 +63,17 @@ on the way out: `QuickJS` on the Node template, `DynamicWorkerExecutor` on the
59
63
  Worker example, and `code executed` when an executor identifies as nothing —
60
64
  a checker that asserts a sandbox it never saw is worse than one that says it
61
65
  does not know ([#368](https://github.com/zackbart/connecta/issues/368)). It
62
- refuses to send a bearer token over remote plaintext HTTP, and it
66
+ refuses to send authentication credentials over remote plaintext HTTP, and it
63
67
  *reports* catalog drift without failing on it — an unclassified downstream tool
64
68
  already fails closed onto `call_destructive_tool`, so drift is a maintainer's
65
69
  next task rather than a broken deployment
66
70
  ([#343](https://github.com/zackbart/connecta/issues/343)).
67
71
 
72
+ For an Access-protected Worker, doctor sends the service-token pair to both
73
+ `/health` and `/mcp`; Access authenticates those requests before connecta runs.
74
+ `CONNECTA_TOKEN` remains the Node and legacy Worker path, and may be supplied
75
+ alongside the Access pair during rollback testing. A partial pair is refused.
76
+
68
77
  ### Configuration
69
78
 
70
79
  Structural seams stay top-level; tuning is grouped by subsystem. Every group is
@@ -74,7 +83,7 @@ optional.
74
83
  | --- | --- | --- |
75
84
  | `connectors` | — (required) | the connector set ([connectors](./connectors.md)) |
76
85
  | `executor` | — (required) | the sandbox `execute_code` runs in ([code mode](./code-mode.md#what-an-executor-must-implement)) |
77
- | `auth?` | none ⇒ open (dev only) | one `InboundAuth` or an array; bearer providers are checked before Clerk ([inbound auth](./auth.md)) |
86
+ | `auth?` | none ⇒ open (dev only) | one `InboundAuth` or an array; bearer providers are checked before interactive providers ([inbound auth](./auth.md)) |
78
87
  | `storage?` | `memoryStorage()` | the one state seam for catalogs, result paging, credentials, and access tokens ([storage](./storage-and-credentials.md)) |
79
88
  | `publicUrl?` | per-request origin | public base URL; an HTTPS value also redirects inbound HTTP |
80
89
  | `logger?` | `console`, prefixed `[connecta]` | `{ debug, info, warn, error }` |
@@ -83,7 +92,7 @@ optional.
83
92
  | `deploymentInfo?` | unset | arbitrary metadata exposed by `/health` |
84
93
  | `activity?` | unset | `{ store, readGate?, deploymentId? }` — payload-free activity storage, an optional operator-read gate, and a stable event label |
85
94
  | `credentials.encryptionKey?` | unset | base64 32-byte AES key for the connector vault. Without it, connectors declaring `credential` warn and their slots stay unmanageable |
86
- | `accessTokens?` | unset | `{ maxActive? }` (default 100) for operator-issued MCP bearer tokens. Requires a Clerk provider, or construction throws ([access tokens](./auth.md#operator-issued-access-tokens)) |
95
+ | `accessTokens?` | unset | `{ maxActive? }` (default 100) for operator-issued MCP bearer tokens. Requires an interactive operator provider, or construction throws ([access tokens](./auth.md#operator-issued-access-tokens)) |
87
96
  | `discovery.concurrency?` | 4 | connector catalogs/status probes in flight at once |
88
97
  | `discovery.catalogTtlSeconds?` | 300 | fresh TTL for cached tool lists |
89
98
  | `discovery.persistCatalog?` | true | persist serializable catalogs as a manifest plus revision-addressed chunks |
@@ -214,7 +223,7 @@ in.
214
223
 
215
224
  | Suite | Covers |
216
225
  | --- | --- |
217
- | `access-tokens.test.ts` | the `AccessTokenManager` — a one-time secret created, authenticated, renamed, and revoked, bounded names and active count, enumerable storage required, a deployment with no Clerk operator refused — and the Clerk-only routes, down to historical activity still resolving a revoked token's name |
226
+ | `access-tokens.test.ts` | the `AccessTokenManager` — a one-time secret created, authenticated, renamed, and revoked, bounded names and active count, enumerable storage required, a deployment with no interactive operator refused — and the operator-only routes, down to historical activity still resolving a revoked token's name |
218
227
  | `activity.test.ts` | payload-free delivery: a rejected async write attaches to `waitUntil` instead of throwing, approved destructive calls record under their real entry point, result-size friction records without retaining the result, and a hallucinated connector id or invented identity is clamped so the event still cannot carry a payload |
219
228
  | `api-connector.test.ts` | `api()` — kind, description, tool defs, dispatch, default args, unknown tools, handler throws, argument validation, and the construction contract |
220
229
  | `bearer.test.ts` | constant-time bearer compare, case-insensitive scheme, 401 challenges, and the retired audience options refusing rather than silently unbinding |
@@ -223,6 +232,7 @@ in.
223
232
  | `catalog-drift.test.ts` | `vettedCatalog()`, `detectCatalogDrift()`, and `withVettedCatalog()`; drift on the registry surface and on `/health`; the connector seam projected rather than echoed; and the drift types being public |
224
233
  | `catalog.test.ts` | lexical ranking and the compact schema renderer — `const`, `allOf` beside siblings, `$ref`, the depth limit, per-schema caching, and 2020-12 keyword compatibility |
225
234
  | `clerk.test.ts` | protected-resource metadata, the browser sign-in config, OAuth and session tokens, cached best-effort activity labels with their caps, the hand-applied `azp` rejection, and the `allowedDomains` allowlist including every lookalike that must not be repaired into a match |
235
+ | `cloudflare-access-auth.test.ts` | trusted `ctx.access` human and service identities, absent/error fail-closed behavior, service-token MCP admission without operator mutation, human same-origin mutation, and the Clerk-to-ambient shell switch |
226
236
  | `cloudflare-provider.test.ts` | `cloudflare()` construction, tool surface, request building, projections, typed failures, and credential test |
227
237
  | `code-first-surface.test.ts` | the seven-tool surface itself — an executor required, every removed option and top-level tool refused, compact always-loaded routing pinned below 1,000 characters, complete on-demand usage served, and `connecta.ui` findable before connector search |
228
238
  | `codemode-compat.test.ts` | the `Executor` seam staying structurally compatible with `@cloudflare/codemode`'s `DynamicWorkerExecutor`, enforced by `tsc` |
@@ -244,7 +254,7 @@ in.
244
254
  | `mixpanel-provider.test.ts` | the Mixpanel proxy, its conditional-input guide and complete reviewed schema-digest manifest |
245
255
  | `notion-provider.test.ts` | Notion's deliberate tool surface, including declined expanded page inputs, request construction, lean projections, both pagination conventions, error mapping, and writes |
246
256
  | `operator-boundary.test.ts` | the operator row of the decisions table, after every mutation route: authentication material managed without moving a declared structure, and the one honest exception — a credential write making a remote catalog appear, which is discovery arriving, not an operator editing the deployment |
247
- | `operator-store.test.ts` | `src/operator-ui/app/store.ts` against a fake browser: the Clerk listener, `gate()`, the generation fence, and the request path |
257
+ | `operator-store.test.ts` | `src/operator-ui/app/store.ts` against a fake browser: the Clerk listener, ambient Access requests without a browser-readable token, `gate()`, the generation fence, and the request path |
248
258
  | `provider-conventions.test.ts` | the conventions a test can hold: hand-written providers refusing schemas they cannot enforce (H5), their compact discovery schemas staying complete (H7), Cloudflare stating its second pagination convention in the schema (H10), and Notion saying it has no escape hatch (H14) |
249
259
  | `provider-registry.test.ts` | all six maintained providers inside real deployments: boot, description, address, catalog, storage, credential, admission, and activity isolation; plus provider-specific discovery and guide contracts |
250
260
  | `registry.test.ts` | construction and id validation, startup warnings, address resolution, version 2 catalog TTL/persistence/completeness, agent-only stale-while-revalidate with cross-request single-flight shared with blocking reads in both start orders, owned teardown, invalidation/fingerprint guards, blocking diagnostics, and broken-connector isolation |
@@ -272,13 +282,13 @@ justification for *not* re-running it in workerd, so "it was easier" is not one.
272
282
  | --- | --- | --- |
273
283
  | `deployment-shapes.test.ts` | the Worker as the only example with a loader-only sandbox, one Node template that is also its own container, the same source running locally and in the container, the Node template's pinned esbuild install-script approval, the full operator surface in both, a template that cannot start on its own `.env.example`, a Worker README naming every optional peer its entrypoint imports, and the initializer's `.gitignore` staying in step | walks the template and example trees with Node filesystem APIs |
274
284
  | `doc-links.test.ts` | the documentation checker itself — local file and fragment resolution, repository URLs resolved back to the checkout, duplicate heading slugs, fenced-code exclusion, and useful failures | spawns the Node checker against filesystem fixtures |
275
- | `doctor-cli.test.ts` | `connecta doctor`'s executor line end to end — the sandbox the deployment reports is the one named, an unidentifiable executor gets an executor-neutral line, and a hostile name is bounded and stripped before it reaches a terminal | spawns the CLI against a Node HTTP deployment over real sockets |
285
+ | `doctor-cli.test.ts` | `connecta doctor`'s executor line and credentials end to end — the sandbox the deployment reports is the one named, an unidentifiable executor gets an executor-neutral line, a hostile name is bounded, and a complete Cloudflare Access service-token pair is accepted while a partial pair is refused | spawns the CLI against a Node HTTP deployment over real sockets |
276
286
  | `drift-check.test.ts` | the maintainer drift checker — hosted-provider credential framing, recorded touched endpoints, a quiet revision bump, clear failures for an unavailable spec/manifest/credential, `$ref` traversal, and one well-formed row per endpoint | spawns the Node checker against filesystem fixtures |
277
287
  | `file-storage.test.ts` | `fileStorage()` across instances, logical TTL plus physical pruning without clobbering a newer value, and corrupt-file quarantine | exercises the Node filesystem storage adapter |
278
288
  | `guest-api-contract-quickjs.test.ts` | the shared guest-contract cases on the real QuickJS executor, including identical caught failure codes and inline describe recovery, its exact absent globals, and blocked runtime imports | runs the contract cases on the Node QuickJS executor |
279
289
  | `node.test.ts` | the `listen()` adapter propagating an HTTP client disconnect through the Web `Request` and the MCP handler into a program's connector call, releasing both admission permits | exercises the Node HTTP adapter over real TCP sockets |
280
290
  | `packed-links.test.ts` | the packed-link gate itself — shipped targets and repository URLs accepted, relative links into unshipped paths and directories rejected with the citation to write instead, reference definitions seen, fenced examples ignored, the changelog exempt | spawns the Node packed-link gate against filesystem fixtures |
281
- | `package-surface.test.ts` | the published boundary — built output shipped, the `exports` map carrying exactly the documented subpaths plus `./package.json`, only generic factories, platform storage kept in examples, Clerk and QuickJS behind optional subpaths, every provider independently importable, and the Cloudflare provider free of bare specifiers | walks the package tree with Node filesystem APIs |
291
+ | `package-surface.test.ts` | the published boundary — built output shipped, the `exports` map carrying exactly the documented subpaths plus `./package.json`, only generic factories, platform storage kept in examples, Clerk and QuickJS behind optional subpaths, dependency-free Cloudflare Access behind its Worker subpath, every provider independently importable, and the Cloudflare API provider free of bare specifiers | walks the package tree with Node filesystem APIs |
282
292
  | `purity.test.ts` | the import-graph guardrail ([architecture](./architecture.md#import-graph-purity)) — the core stays Workers-clean | walks the source import graph with Node filesystem APIs |
283
293
  | `quickjs-child-entry.test.ts` | a missing QuickJS child entry failing before `fork()`, with the expected path and the bundler-externalization constraint | mocks Node child-process and filesystem APIs |
284
294
  | `quickjs-child-stderr.test.ts` | abnormal child exits retaining only an 8 KiB stderr tail, included in the parent-side diagnostic | mocks Node child-process streams |
@@ -319,10 +329,15 @@ confinement does too.
319
329
  - **A connector with no `verifyState` refuses every callback.** That is the
320
330
  designed behavior, not a bug: handing an unverified code to `finishAuth` is
321
331
  the vulnerability. The startup warning names the connector.
322
- - **401 loops from a client that cannot discover auth.** The client must reach
332
+ - **401 loops from a Clerk client that cannot discover auth.** The client must reach
323
333
  the open `/.well-known/oauth-protected-resource` (and the `/mcp` variant);
324
334
  confirm CORS and the Clerk keys, and that DCR is enabled on the Clerk
325
335
  instance.
336
+ - **An Access-protected MCP client receives redirects or loops.** Enable Managed
337
+ OAuth on the Access application and use an RFC 8707-capable client. Access,
338
+ not connecta, must answer the unauthenticated challenge and `/.well-known/`
339
+ metadata. Do not bypass those routes. For unattended automation, use an
340
+ Access Service Auth policy and service-token headers instead.
326
341
  - **No sessions and no server push, by design.** The transport is stateless.
327
342
  Scope resolves per request, which is also where the MCP spec has arrived.
328
343
  - **A tool that should be callable from a program is not.** Only tools
@@ -39,10 +39,25 @@ their walkthrough, which is the honest version of the same page count.
39
39
  | `src/operator-ui/browser.css` | One stylesheet, inlined into the shell. |
40
40
  | `src/operator-ui/generated.ts` | The build output: the bundle and the stylesheet as two exported strings. |
41
41
 
42
- The server renders a mount point, not a page. Branding, the Clerk loader, and
43
- every operator-configured URL stay in `src/ui.ts`, where they are gated before
44
- they can become an attribute; the bundle renders everything that has a state.
45
- Two roots share one store: `#operatorNav` and `#operatorContent`.
42
+ The server renders a mount point, not a page. Branding, the optional Clerk
43
+ loader, and every operator-configured URL stay in `src/ui.ts`, where they are
44
+ gated before they can become an attribute; the bundle renders everything that
45
+ has a state. Two roots share one store: `#operatorNav` and `#operatorContent`.
46
+
47
+ Cloudflare Access is ambient browser auth. When the current Worker invocation
48
+ has `ctx.access`, the shell selects the `cloudflare-access` UI mode, emits no
49
+ Clerk loader, and sends no browser-readable token. Same-origin fetch includes
50
+ the HttpOnly `CF_Authorization` cookie, Access admits it at the edge, and the
51
+ server reads the resulting runtime identity. Sign out navigates to
52
+ `/cdn-cgi/access/logout`. Mutations still require an exact same-origin
53
+ `Origin`; an ambient cookie does not weaken the CSRF boundary.
54
+
55
+ This runtime selection is the Clerk migration seam. A deployment may contain
56
+ both providers: before Worker-level Access is attached, the data-free shell
57
+ selects Clerk; after Access supplies `ctx.access`, it selects ambient auth. That
58
+ is not two same-hostname gates running in parallel. Access is upstream and a
59
+ request it rejects never reaches Clerk. Keeping Clerk in the array preserves a
60
+ code-level rollback after Access is detached.
46
61
 
47
62
  The Clerk loader is intentionally blocking. The inline operator bundle calls
48
63
  `boot()` as soon as the parser reaches the end of the body, so a deferred Clerk
@@ -58,7 +73,8 @@ the same ordering.
58
73
  credential, token, and activity data arrives only through the authenticated
59
74
  `/ui/*` APIs, and the shell is identical whether or not a caller is signed in.
60
75
  - **One store, one identity.** `store.ts` is the only file that touches `fetch`,
61
- `localStorage`, or Clerk. Every request carries the current session's token,
76
+ `localStorage`, Clerk, or the ambient Access mode. Every token-bearing request carries the current session's token,
77
+ while Access requests deliberately carry none,
62
78
  and every response is dropped unless the identity that asked for it is still
63
79
  the one on screen. `resetIdentity` replaces all identity-scoped state at once
64
80
  and bumps a generation that work already in flight compares itself against.
@@ -57,7 +57,7 @@ exist so far:
57
57
  | --- | --- | --- |
58
58
  | **pre-template** | before 0.10.2 | no `connecta init` existed; hand-written, or copied from the retired `examples/node` |
59
59
  | **A** | 0.10.2 – 0.15.1 | `.env.example`, `.gitignore`, `AGENTS.md`, `CLAUDE.md`, `README.md`, `package.json`, `src/index.ts`, `tsconfig.json` |
60
- | **B** | 0.16.0 – 0.20.0 | adds `.dockerignore`, `Dockerfile`, `docker-compose.yml`, and `src/file-activity.ts`; `src/index.ts` grows the four commented operator blocks; `.env.example` ships `CONNECTA_TOKEN=` empty |
60
+ | **B** | 0.16.0 – 0.21.0 | adds `.dockerignore`, `Dockerfile`, `docker-compose.yml`, and `src/file-activity.ts`; `src/index.ts` grows the four commented operator blocks; `.env.example` ships `CONNECTA_TOKEN=` empty |
61
61
 
62
62
  Generation A is a decade in template years and identifying it precisely does
63
63
  not matter, because you are about to reconstruct it exactly rather than guess
@@ -106,7 +106,7 @@ know what to preserve, once to know what to re-verify at the end.
106
106
  ### Bump the pin and install
107
107
 
108
108
  ```sh
109
- npm pkg set dependencies.@zackbart/connecta=0.20.0
109
+ npm pkg set dependencies.@zackbart/connecta=0.21.0
110
110
  npm install
111
111
  ```
112
112
 
@@ -130,7 +130,7 @@ Generate the *current* template beside the base you already made, into the same
130
130
  `$SCRATCH`:
131
131
 
132
132
  ```sh
133
- (cd "$SCRATCH" && npx @zackbart/connecta@0.20.0 init current)
133
+ (cd "$SCRATCH" && npx @zackbart/connecta@0.21.0 init current)
134
134
  ```
135
135
 
136
136
  You now have a three-way merge with a real base: `$SCRATCH/base` is what this
@@ -186,7 +186,7 @@ A deployment older than 0.10.2 has no base to diff against. Do not try to
186
186
  manufacture one. Instead:
187
187
 
188
188
  1. `SCRATCH=$(mktemp -d)`, then
189
- `(cd "$SCRATCH" && npx @zackbart/connecta@0.20.0 init current)` — there is no
189
+ `(cd "$SCRATCH" && npx @zackbart/connecta@0.21.0 init current)` — there is no
190
190
  `base` leg here, only the current template to read from.
191
191
  2. Copy `$SCRATCH/current` into the deployment file by file, **skipping
192
192
  `src/index.ts`**.
@@ -207,6 +207,78 @@ first, so cross them bottom-up: start at the oldest one still above this
207
207
  deployment's pin and work back up the page, because each boundary assumes the
208
208
  older ones are already done.
209
209
 
210
+ ### 0.20.0 → 0.21.0
211
+
212
+ This is additive for Node and existing Clerk deployments. The new Worker path
213
+ uses Cloudflare Access identity directly and removes Clerk only after the edge
214
+ cutover has been verified. An agent can perform every repository edit; a human
215
+ must attach Access, choose its policy, create service credentials, and enable
216
+ Managed OAuth in the Cloudflare dashboard.
217
+
218
+ For a Worker currently using Clerk, keep rollback live through the cutover:
219
+
220
+ 1. Bump and install 0.21.0. Add the new provider **before** the existing Clerk
221
+ provider, but remove nothing:
222
+
223
+ ```ts
224
+ import { cloudflareAccessAuth } from
225
+ "@zackbart/connecta/auth/cloudflare-access";
226
+ import { clerkAuth } from "@zackbart/connecta/auth/clerk";
227
+
228
+ auth: [
229
+ cloudflareAccessAuth(),
230
+ // Keep the deployment's existing options and secrets unchanged.
231
+ clerkAuth({ /* existing configuration */ }),
232
+ ],
233
+ ```
234
+
235
+ Before Access is attached, the new provider fails closed and the operator
236
+ shell selects Clerk. Deploy this state and run doctor with the existing
237
+ `CONNECTA_TOKEN`. This separates the package/code change from the edge
238
+ change and proves the old path still works.
239
+
240
+ 2. In Cloudflare, attach Access to the Worker itself, apply the intended human
241
+ policy, and enable Managed OAuth. Through the API this is an Access
242
+ application destination of `{ "type": "worker", "worker_id": "<script
243
+ tag>" }`, not a hostname application for the `workers.dev` URL: the latter
244
+ gates traffic but does not provide `ctx.access`. Create an Access service
245
+ token and a **Service Auth** policy for doctor and fully unattended clients.
246
+ Do not create a bypass for `/.well-known/*`; Managed OAuth owns that
247
+ discovery surface.
248
+
249
+ 3. Reconnect interactive MCP clients to `<PUBLIC_URL>/mcp`. Their old Clerk
250
+ OAuth tokens are not Cloudflare credentials, so each client performs one new
251
+ browser authorization. An agent can edit client configuration and start the
252
+ flow; the user still completes the identity-provider prompt. Move CI, cron,
253
+ and server-to-server callers from connecta bearers to the two Access service
254
+ headers. The cutover warning is literal: once Access is attached, a static
255
+ bearer or `cta_…` token by itself is stopped at the edge before connecta can
256
+ inspect it.
257
+
258
+ 4. Verify the edge path:
259
+
260
+ ```sh
261
+ CF_ACCESS_CLIENT_ID=… CF_ACCESS_CLIENT_SECRET=… \
262
+ npx connecta doctor --url https://connecta.example.workers.dev
263
+ ```
264
+
265
+ Open `/` as a human and exercise any enabled credential, token, and OAuth
266
+ controls. A service token may pass doctor and MCP but must receive 403 from
267
+ operator mutations.
268
+
269
+ 5. After an observation window, remove `clerkAuth`, its import,
270
+ `@clerk/backend`, and the Clerk variables/secrets. Until then they are inert
271
+ behind Access but preserve rollback. Rollback order matters: detach Access
272
+ first, then the untouched Clerk sessions and connecta bearers reach the
273
+ Worker again. Reverting code first cannot help a request the edge still
274
+ blocks. There is no storage migration and no token-format conversion.
275
+
276
+ If the Worker exposes an intentionally public connector route, create a
277
+ more-specific hostname/path Access application with a Bypass policy for that
278
+ route only. `/health`, downstream OAuth callbacks, operator shells, and MCP are
279
+ private under the canonical whole-Worker shape; doctor knows how to authenticate
280
+ its health request.
281
+
210
282
  ### 0.19.0 → 0.20.0
211
283
 
212
284
  Three intake paths become deliberately strict. None changes storage, the two
package/ethos.md CHANGED
@@ -1,9 +1,7 @@
1
1
  # connecta — ethos
2
2
 
3
3
  What connecta is, what it refuses to be, and the invariants every change must
4
- preserve. Deliberately terse: when a change contradicts a line here, either the
5
- change is wrong or this file needs amending — in that order, and amending it is
6
- a design decision, not a drive-by edit.
4
+ preserve. A contradiction needs a design decision, not a drive-by edit.
7
5
 
8
6
  ## What this is
9
7
 
@@ -78,6 +76,7 @@ CHANGELOG, not here.
78
76
  | MRTR / `input_required` passthrough | gated | relayable statelessly; no host or downstream emits it yet ([#176](https://github.com/zackbart/connecta/issues/176)) |
79
77
  | Downstream `ttlMs` cache hints | gated | needs refresh-churn evidence ([#206](https://github.com/zackbart/connecta/issues/206)) |
80
78
  | Downstream MCP Apps template passthrough | gated | needs a downstream that ships one ([#266](https://github.com/zackbart/connecta/issues/266)) |
79
+ | Worker Access inbound auth | provisional | Managed OAuth and Clerk migration need production evidence ([#506](https://github.com/zackbart/connecta/issues/506)) |
81
80
  | Program UI tool calls | removed | the read bridge added a second contract without improving agent data retrieval; views are display-only again ([#287](https://github.com/zackbart/connecta/issues/287), [#484](https://github.com/zackbart/connecta/issues/484)) |
82
81
 
83
82
  ## Invariants
@@ -1,9 +1,9 @@
1
1
  # connecta — Cloudflare Worker example
2
2
 
3
3
  A deployable Worker that aggregates a downstream remote MCP and an in-code HTTP
4
- API connector, guarded by Clerk OAuth *and* a static bearer token, with state in
5
- a KV namespace. Its required Worker Loader binding backs the seven-tool surface
6
- and requires the Workers Paid plan.
4
+ API connector, guarded by Cloudflare Access, with state in a KV namespace. Its
5
+ required Worker Loader binding backs the seven-tool surface and requires the
6
+ Workers Paid plan.
7
7
 
8
8
  This is also the **starting template for a deployment**: a real deployment
9
9
  should be its own repository that pins an exact `@zackbart/connecta` version and
@@ -37,18 +37,27 @@ npm install # from the package root
37
37
  wrangler kv namespace create CONNECTA_KV # paste the id into wrangler.jsonc
38
38
 
39
39
  cd examples/worker
40
- wrangler secret put SUPPORT_TOKEN # one headless client
41
- wrangler secret put EXEC_TOKEN # another headless client
42
- wrangler secret put CLERK_SECRET_KEY
43
40
  wrangler secret put DOWNSTREAM_TOKEN
44
41
  wrangler secret put CREDENTIAL_ENCRYPTION_KEY # base64 32-byte AES key
45
42
  wrangler deploy
46
43
  ```
47
44
 
48
- `PUBLIC_URL` and `CLERK_PUBLISHABLE_KEY` are plain vars in `wrangler.jsonc`.
49
- Enable Dynamic Client Registration on the Clerk instance (OAuth Applications
50
- DCR) so Claude/Cursor can self-register full walkthrough in
51
- [setting up Clerk](../../documentation/auth.md).
45
+ `PUBLIC_URL` is a plain var in `wrangler.jsonc`. After the first deploy, attach
46
+ Cloudflare Access to the Worker itself (the API destination type is `worker`,
47
+ not a hostname application) and choose the account, email-domain, or
48
+ advanced Zero Trust policy that owns admission. Enable **Managed OAuth** on
49
+ that Access application for interactive MCP clients. Access then serves OAuth
50
+ discovery and turns the client's opaque token into the trusted `ctx.access`
51
+ identity connecta reads. A cron job or CI client uses an Access service token
52
+ instead.
53
+
54
+ Cloudflare's [Worker Access guide](https://developers.cloudflare.com/workers/configuration/cloudflare-access/)
55
+ owns the dashboard/API steps; its [Managed OAuth guide](https://developers.cloudflare.com/cloudflare-one/access-controls/applications/http-apps/managed-oauth/)
56
+ owns client registration, redirect allowlists, and token lifetimes.
57
+
58
+ The checked-in `access.dev` block gives `wrangler dev` a local operator
59
+ identity. Remove the block to test the missing-Access refusal. It has no effect
60
+ on a deployed Worker's production identity.
52
61
 
53
62
  ### Copied into its own repository
54
63
 
@@ -57,24 +66,22 @@ dependency this file imports. A copy with its own `package.json` installs three
57
66
  things, because two of them are not part of connecta and never install with it:
58
67
 
59
68
  ```sh
60
- npm install @zackbart/connecta @cloudflare/codemode @clerk/backend
69
+ npm install @zackbart/connecta @cloudflare/codemode
61
70
  ```
62
71
 
63
- Both are optional peers of `@zackbart/connecta` declared in its manifest,
64
- never installed with it, and each carrying the range this release supports.
65
- `@cloudflare/codemode` is the executor behind `execute_code`, published as
72
+ `@cloudflare/codemode` is the optional peer behind `execute_code`, declared in
73
+ connecta's manifest but never installed with it, and published as
66
74
  `^0.4.4 || ^0.5.0`: install a version inside that and npm stays quiet, install
67
75
  one outside and npm says so at install time instead of leaving a Worker to
68
76
  discover the skew in production ([#376](https://github.com/zackbart/connecta/issues/376)).
69
77
 
70
- `@clerk/backend` is the peer behind `@zackbart/connecta/auth/clerk`, which
71
- `src/index.ts` imports at the top level, so wrangler must resolve it at
72
- build time. Miss it and the build stops at
73
- `Could not resolve "@clerk/backend"`, which is a missing peer rather than a
74
- broken example. Drop `clerkAuth` from `auth` if this deployment has no operator
75
- sign-in, and the peer goes with it — but read
76
- [the operator surface](#the-operator-surface) first, because a deployment
77
- without it can never write a credential or issue an access token.
78
+ `cloudflareAccessAuth()` has no dependency of its own. A deployment keeping
79
+ Clerk for rollback still installs `@clerk/backend` and keeps the commented
80
+ provider shape in `src/index.ts` until the migration is verified.
81
+
82
+ ```sh
83
+ npm install @clerk/backend # migration window only
84
+ ```
78
85
 
79
86
  Then point an MCP client at `<PUBLIC_URL>/mcp`, and open `<PUBLIC_URL>/` for
80
87
  Connections. Credentials is at `/credentials`, named MCP access tokens are at
@@ -87,11 +94,11 @@ the next section for what turns each one on.
87
94
  This example ships the whole operator feature set. Three quarters of it is on
88
95
  as deployed; the fourth needs a database, so it is commented in place.
89
96
 
90
- **Operator sign-in** is the `clerkAuth` entry in `src/index.ts`, alongside two
91
- static bearers. The split is deliberate: a bearer is a client key that may call
92
- tools and read connector status, while writing a credential or issuing an
93
- access token requires an interactive Clerk identity. Narrow who that can be
94
- with `allowedDomains`, or with a `gate` for anything a domain cannot express.
97
+ **Operator sign-in** is the `cloudflareAccessAuth()` entry in `src/index.ts`.
98
+ Access authenticates before the Worker runs. A human Access identity can use
99
+ MCP and operator pages; a service-token identity can use MCP but cannot write a
100
+ credential, run downstream OAuth, or issue a connecta token. Narrow admission
101
+ in the Access policy rather than repeating email domains or groups in code.
95
102
 
96
103
  **The credential vault** is `credentials: { encryptionKey: … }`, backed by the
97
104
  same KV namespace as everything else and encrypted with the
@@ -116,12 +123,18 @@ shape on `echo` is exactly it) or use a provider connector such as `notion()`,
116
123
  which declares its own, and Credentials appears for a signed-in operator on the
117
124
  next load.
118
125
 
119
- **Access tokens** are `accessTokens: {}`. A signed-in operator mints named,
126
+ **Access tokens** are `accessTokens: {}`. A signed-in human operator mints named,
120
127
  revocable Bearer tokens at `/tokens` for header-capable clients that will not do
121
128
  OAuth. Secrets are shown once and only their hashes enter KV; a lost token is
122
129
  reissued, never recovered. Note the KV caveat above — revocation is visible
123
130
  everywhere only as fast as the namespace converges.
124
131
 
132
+ Worker-level Access still runs before these tokens. A `cta_…` token therefore
133
+ does not reach connecta by itself; retain the feature as a rollback path or for
134
+ a caller that already supplies separate Access service-token headers. Normal
135
+ interactive MCP clients should use Managed OAuth, and unattended clients should
136
+ use Access service tokens.
137
+
125
138
  **Activity** is the commented block in `src/index.ts` and the commented
126
139
  `d1_databases` binding in `wrangler.jsonc`; the section below creates the
127
140
  database and applies the schema.
@@ -134,10 +147,17 @@ never the connector set, the tool catalog, or its annotations.
134
147
  this on: connector count, executor, seven tools. The executor it names is this
135
148
  one — `DynamicWorkerExecutor executed`, not the Node template's QuickJS, which
136
149
  is what doctor used to claim everywhere
137
- ([#368](https://github.com/zackbart/connecta/issues/368)). It carries a bearer, and a
138
- bearer learns the model-facing surface rather than the deployment's
139
- configuration topology. Confirm the operator surface the way an operator will:
140
- sign in at `<PUBLIC_URL>/` and check that Tokens is live. Credentials joins it
150
+ ([#368](https://github.com/zackbart/connecta/issues/368)). Against Access it
151
+ carries `CF_ACCESS_CLIENT_ID` and `CF_ACCESS_CLIENT_SECRET`, and the service
152
+ identity learns the model-facing surface rather than deployment topology:
153
+
154
+ ```sh
155
+ CF_ACCESS_CLIENT_ID=… CF_ACCESS_CLIENT_SECRET=… \
156
+ npx connecta doctor --url "$PUBLIC_URL"
157
+ ```
158
+
159
+ Confirm the operator surface the way an operator will: sign in at
160
+ `<PUBLIC_URL>/` and check that Tokens is live. Credentials joins it
141
161
  once a connector declares a `credential` slot, and Activity once the D1 wiring
142
162
  below is on — the nav shows a page when the deployment can actually serve it,
143
163
  so a missing page is the honest report that its half is still off.
@@ -2,9 +2,10 @@
2
2
  * connecta on Cloudflare Workers.
3
3
  *
4
4
  * One MCP endpoint aggregating a downstream remote MCP and an HTTP API, guarded
5
- * by Clerk OAuth *and* a static bearer token, with OAuth/cache state in a KV
6
- * namespace. The required Worker Loader binding in wrangler.jsonc backs the
7
- * seven-tool surface.
5
+ * by Cloudflare Access, with OAuth/cache state in a KV namespace. Access
6
+ * authenticates the request before this Worker runs and supplies the trusted
7
+ * identity through ctx.access. The required Worker Loader binding in
8
+ * wrangler.jsonc backs the seven-tool surface.
8
9
  *
9
10
  * The operator surface is wired here except for activity history, which needs
10
11
  * a database this example does not create for you: sign-in, the credential
@@ -15,19 +16,16 @@
15
16
  * installed `@zackbart/connecta` package):
16
17
  * 1. `npm install` in the connecta package root (../../ from here) so the
17
18
  * package import and wrangler resolve. A copy in its own repository
18
- * installs `@zackbart/connecta @cloudflare/codemode @clerk/backend`
19
- * instead the last two are not part of connecta, and the Clerk import
20
- * below is an optional peer wrangler resolves at build time.
19
+ * installs `@zackbart/connecta @cloudflare/codemode` instead. Codemode is
20
+ * an optional peer; a migrating deployment also keeps `@clerk/backend`
21
+ * until it removes the commented rollback provider below.
21
22
  * 2. Create a KV namespace and put its id in wrangler.jsonc under `kv_namespaces`.
22
23
  * 3. Set secrets:
23
- * wrangler secret put SUPPORT_TOKEN
24
- * wrangler secret put EXEC_TOKEN
25
- * wrangler secret put CLERK_SECRET_KEY
26
24
  * wrangler secret put DOWNSTREAM_TOKEN
27
25
  * wrangler secret put CREDENTIAL_ENCRYPTION_KEY
28
- * and CLERK_PUBLISHABLE_KEY + PUBLIC_URL as plain vars in wrangler.jsonc.
29
- * 4. Enable Dynamic Client Registration in the Clerk dashboard
30
- * (OAuth Applications -> DCR toggle) so Claude/Cursor can self-register.
26
+ * and PUBLIC_URL as a plain var in wrangler.jsonc.
27
+ * 4. Attach Cloudflare Access to this Worker. Enable Managed OAuth on the
28
+ * Access application for interactive MCP clients.
31
29
  * 5. Use the Workers Paid plan required by the `worker_loaders` binding.
32
30
  * 6. `wrangler deploy` from this folder (examples/worker), where wrangler.jsonc
33
31
  * lives. Point your MCP client at `<PUBLIC_URL>/mcp`.
@@ -35,23 +33,21 @@
35
33
  import { DynamicWorkerExecutor } from "@cloudflare/codemode";
36
34
  import {
37
35
  api,
38
- bearerToken,
39
36
  createConnecta,
40
37
  remoteMcp,
41
38
  } from "@zackbart/connecta";
42
- import { clerkAuth } from "@zackbart/connecta/auth/clerk";
39
+ import { cloudflareAccessAuth } from "@zackbart/connecta/auth/cloudflare-access";
40
+ // Rollback for a deployment migrating from Clerk:
41
+ // import { clerkAuth } from "@zackbart/connecta/auth/clerk";
43
42
  import { cloudflareKvStorage } from "./cloudflare-kv.js";
44
43
  // Activity history, off by default because it needs a D1 database.
45
44
  // import { d1ActivityStore } from "./d1-activity.js";
46
45
 
47
46
  interface Env {
48
47
  CONNECTA_KV: KVNamespace;
49
- /** Bearer token for one headless client in this deployment's audience. */
50
- SUPPORT_TOKEN: string;
51
- /** Bearer token for another headless client in the same audience. */
52
- EXEC_TOKEN: string;
53
- CLERK_PUBLISHABLE_KEY: string;
54
- CLERK_SECRET_KEY: string;
48
+ // Keep these during a Clerk migration until Access has been verified:
49
+ // CLERK_PUBLISHABLE_KEY: string;
50
+ // CLERK_SECRET_KEY: string;
55
51
  /**
56
52
  * Base64 32-byte AES key encrypting operator-managed credentials in KV.
57
53
  * Unset means no vault: /credentials stays read-only and connecta says so at
@@ -75,22 +71,19 @@ function build(env: Env) {
75
71
  storage: cloudflareKvStorage(env.CONNECTA_KV),
76
72
  executor: new DynamicWorkerExecutor({ loader: env.LOADER }),
77
73
  auth: [
78
- // Multiple credentials may identify callers in one deployment. Every
79
- // admitted caller reaches this deployment's deliberate connector set.
80
- bearerToken(env.SUPPORT_TOKEN, {
81
- subjectId: "support-team",
82
- }),
83
- bearerToken(env.EXEC_TOKEN, {
84
- subjectId: "exec-team",
85
- }),
86
- // The operator signs in with Clerk. Restrict who may sign in with
87
- // `allowedDomains` (or a `gate`, for anything a domain cannot express).
88
- clerkAuth({
89
- publishableKey: env.CLERK_PUBLISHABLE_KEY,
90
- secretKey: env.CLERK_SECRET_KEY,
91
- publicUrl: env.PUBLIC_URL,
92
- // allowedDomains: ["acme.com"],
93
- }),
74
+ // Access owns admission policy. A human identity may use MCP and the
75
+ // operator pages; a service token may use MCP but cannot mutate operator
76
+ // state. Neither path asks connecta to parse a JWT.
77
+ cloudflareAccessAuth(),
78
+ // Leave the previous Clerk provider below this entry during migration.
79
+ // It is a rollback path until Worker-level Access is detached; Access
80
+ // itself decides whether a request reaches this array.
81
+ // clerkAuth({
82
+ // publishableKey: env.CLERK_PUBLISHABLE_KEY,
83
+ // secretKey: env.CLERK_SECRET_KEY,
84
+ // publicUrl: env.PUBLIC_URL,
85
+ // allowedDomains: ["acme.com"],
86
+ // }),
94
87
  ],
95
88
  // Connectors that declare a `credential` slot become editable at
96
89
  // /credentials, encrypted with this key before anything reaches KV. A
@@ -105,8 +98,9 @@ function build(env: Env) {
105
98
  // `echo`, or use a provider connector like `notion()`, which declares its
106
99
  // own) and the page appears on the next load.
107
100
  credentials: { encryptionKey: env.CREDENTIAL_ENCRYPTION_KEY },
108
- // Eligible Clerk operators can create named, revocable MCP Bearer tokens
109
- // at /tokens. Secrets are shown once; only their hashes enter KV.
101
+ // Eligible human operators can create named, revocable MCP Bearer tokens
102
+ // at /tokens. Under Worker-level Access those tokens are a rollback tool,
103
+ // not standalone edge credentials: Access still runs before connecta.
110
104
  accessTokens: {},
111
105
  // Payload-free activity at /activity, off until a database exists to hold
112
106
  // it. Uncomment the `d1_databases` binding in wrangler.jsonc, apply the
@@ -6,12 +6,20 @@
6
6
  "compatibility_flags": ["nodejs_compat"],
7
7
  "observability": { "enabled": true },
8
8
 
9
- // Plain vars. Secrets (SUPPORT_TOKEN, EXEC_TOKEN, CLERK_SECRET_KEY,
10
- // CREDENTIAL_ENCRYPTION_KEY, DOWNSTREAM_TOKEN) are set with
9
+ // Plain vars. Secrets (CREDENTIAL_ENCRYPTION_KEY, DOWNSTREAM_TOKEN) are set with
11
10
  // `wrangler secret put <NAME>`, not here.
12
11
  "vars": {
13
- "PUBLIC_URL": "https://connecta.example.workers.dev",
14
- "CLERK_PUBLISHABLE_KEY": "pk_test_replace-me"
12
+ "PUBLIC_URL": "https://connecta.example.workers.dev"
13
+ },
14
+
15
+ // Local-only Access identity. Remove this block to exercise the
16
+ // unauthenticated path in `wrangler dev`; production identity comes from the
17
+ // Worker-level Access application, not from this file.
18
+ "access": {
19
+ "dev": {
20
+ "aud": "connecta-local",
21
+ "identity": { "email": "operator@example.com" }
22
+ }
15
23
  },
16
24
 
17
25
  // Create with `wrangler kv namespace create CONNECTA_KV` and paste the id.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zackbart/connecta",
3
- "version": "0.20.0",
3
+ "version": "0.21.0",
4
4
  "type": "module",
5
5
  "sideEffects": false,
6
6
  "description": "One MCP to rule them all — a single MCP endpoint aggregating many downstream connectors behind a code-first surface of seven meta-tools.",
@@ -63,6 +63,10 @@
63
63
  "types": "./dist/auth/clerk.d.ts",
64
64
  "import": "./dist/auth/clerk.js"
65
65
  },
66
+ "./auth/cloudflare-access": {
67
+ "types": "./dist/auth/cloudflare-access.d.ts",
68
+ "import": "./dist/auth/cloudflare-access.js"
69
+ },
66
70
  "./providers/cloudflare": {
67
71
  "types": "./dist/providers/cloudflare.d.ts",
68
72
  "import": "./dist/providers/cloudflare.js"
@@ -15,7 +15,7 @@
15
15
  "typecheck": "tsc --noEmit"
16
16
  },
17
17
  "dependencies": {
18
- "@zackbart/connecta": "0.20.0",
18
+ "@zackbart/connecta": "0.21.0",
19
19
  "quickjs-emscripten": "0.32.0"
20
20
  },
21
21
  "devDependencies": {