@zackbart/connecta 0.21.2 → 0.22.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 (63) hide show
  1. package/CHANGELOG.md +53 -0
  2. package/README.md +7 -0
  3. package/dist/access-tokens.d.ts +2 -2
  4. package/dist/access-tokens.js +14 -2
  5. package/dist/connectors/api.d.ts +2 -0
  6. package/dist/connectors/api.js +1 -0
  7. package/dist/connectors/remote-mcp.d.ts +2 -0
  8. package/dist/connectors/remote-mcp.js +6 -0
  9. package/dist/credentials.d.ts +6 -6
  10. package/dist/credentials.js +25 -21
  11. package/dist/identity.d.ts +4 -0
  12. package/dist/identity.js +17 -0
  13. package/dist/index.d.ts +16 -2
  14. package/dist/index.js +6 -1
  15. package/dist/meta-tools.js +7 -2
  16. package/dist/operator-ui/generated.js +1 -1
  17. package/dist/operator-ui/model.d.ts +4 -2
  18. package/dist/operator-ui/view.js +1 -1
  19. package/dist/providers/cloudflare.d.ts +2 -0
  20. package/dist/providers/cloudflare.js +1 -0
  21. package/dist/providers/linear.d.ts +2 -0
  22. package/dist/providers/linear.js +1 -0
  23. package/dist/providers/mixpanel.d.ts +2 -0
  24. package/dist/providers/mixpanel.js +1 -0
  25. package/dist/providers/notion.d.ts +2 -0
  26. package/dist/providers/notion.js +1 -0
  27. package/dist/providers/revenuecat.d.ts +2 -0
  28. package/dist/providers/revenuecat.js +1 -0
  29. package/dist/providers/stripe.d.ts +2 -0
  30. package/dist/providers/stripe.js +1 -0
  31. package/dist/registry.d.ts +25 -0
  32. package/dist/registry.js +200 -4
  33. package/dist/routes/access-tokens.js +2 -2
  34. package/dist/routes/activity.js +4 -1
  35. package/dist/routes/credentials.js +31 -12
  36. package/dist/routes/mcp.js +17 -2
  37. package/dist/routes/oauth.js +55 -11
  38. package/dist/routes/shared.d.ts +20 -4
  39. package/dist/routes/shared.js +92 -24
  40. package/dist/routes/ui.js +32 -13
  41. package/dist/types.d.ts +28 -2
  42. package/dist/ui.d.ts +3 -3
  43. package/dist/ui.js +18 -5
  44. package/dist/version.d.ts +1 -1
  45. package/dist/version.js +1 -1
  46. package/documentation/architecture.md +14 -8
  47. package/documentation/auth.md +89 -9
  48. package/documentation/code-mode.md +2 -2
  49. package/documentation/connectors.md +13 -0
  50. package/documentation/meta-tools.md +4 -3
  51. package/documentation/operations.md +3 -1
  52. package/documentation/operator-ui.md +13 -4
  53. package/documentation/request-admission.md +2 -1
  54. package/documentation/storage-and-credentials.md +42 -4
  55. package/documentation/upgrading.md +35 -4
  56. package/ethos.md +8 -8
  57. package/examples/worker/AGENTS.md +44 -0
  58. package/examples/worker/README.md +63 -14
  59. package/examples/worker/src/index.ts +26 -22
  60. package/package.json +1 -1
  61. package/templates/node/README.md +7 -0
  62. package/templates/node/package.json +1 -1
  63. package/templates/node/src/index.ts +13 -4
@@ -785,8 +785,8 @@ The human message is unchanged; a mismatched frame is ordinary untyped prose.
785
785
 
786
786
  ## Changes from earlier code mode
787
787
 
788
- Six behaviors changed with this contract, matching the changelog's Unreleased
789
- entry. Programs that ran before still run.
788
+ Six behaviors changed with this contract, matching the 0.10.0 release notes.
789
+ Programs that ran before still run.
790
790
 
791
791
  - **Caught Connecta failures expose their classification** (`E1`, `X11`). Their human message and thrown semantics stay unchanged; `code`, `retryable`, and `details` are additive.
792
792
 
@@ -1,5 +1,18 @@
1
1
  # Connectors
2
2
 
3
+ Every connector may set `authScope: "shared" | "personal"`. Shared is the
4
+ default and keeps one deployment-wide downstream grant. Personal auth requires
5
+ a stable human principal and partitions connector state, credentials, OAuth,
6
+ catalogs, and observed shapes by that principal. Connector visibility is a
7
+ separate deployment rule under `identity.connectorAccess`; hiding a connector
8
+ does not change who owns its auth. See [shared and personal auth](./storage-and-credentials.md#shared-and-personal-auth).
9
+
10
+ `authScope` partitions connecta-owned context, not arbitrary variables captured
11
+ by connector code. A custom personal connector must read auth from
12
+ `ctx.credential` or `ctx.storage`; a secret closed over by its handler remains
13
+ shared JavaScript state. `remoteMcp()` rejects the equivalent mistake when
14
+ literal headers are combined with personal scope.
15
+
3
16
  Connectors are the boundary between Connecta's fixed meta-tool surface and
4
17
  downstream capabilities. Prefer a prebuilt connection when Connecta maintains
5
18
  one for the provider. Use `api()` to define a deliberate HTTP API surface and
@@ -307,9 +307,10 @@ credential.
307
307
 
308
308
  The tool accepts no secret. `force` applies only to OAuth and may discard its
309
309
  stored grant before restarting consent. Static credential values are written
310
- only through the same-origin, Clerk-operator credential route. After OAuth
311
- consent or an operator update, retry the original operation; a static update is
312
- read from the vault on the next call and needs no redeploy.
310
+ only through the same-origin interactive-user credential route, and only for a
311
+ connector visible to that user. After OAuth consent or a human update, retry
312
+ the original operation; a static update is read from the vault on the next call
313
+ and needs no redeploy.
313
314
 
314
315
  ## Routing recovery
315
316
 
@@ -84,6 +84,7 @@ optional.
84
84
  | `connectors` | — (required) | the connector set ([connectors](./connectors.md)) |
85
85
  | `executor` | — (required) | the sandbox `execute_code` runs in ([code mode](./code-mode.md#what-an-executor-must-implement)) |
86
86
  | `auth?` | none ⇒ open (dev only) | one `InboundAuth` or an array; bearer providers are checked before interactive providers ([inbound auth](./auth.md)) |
87
+ | `identity?` | all connectors; every interactive human is an operator | `{ connectorAccess?, operatorAccess? }` derives the request's connector view and shared-auth authority from its authenticated identity ([principals](./auth.md#principals-visibility-and-operators)) |
87
88
  | `storage?` | `memoryStorage()` | the one state seam for catalogs, result paging, credentials, and access tokens ([storage](./storage-and-credentials.md)) |
88
89
  | `publicUrl?` | per-request origin | public base URL; an HTTPS value also redirects inbound HTTP |
89
90
  | `logger?` | `console`, prefixed `[connecta]` | `{ debug, info, warn, error }` |
@@ -247,6 +248,7 @@ in.
247
248
  | `executor-admission.test.ts` | the portable bounded FIFO both pools use: active and queue ceilings, stable retryable overload, queue timeout, cancellation removal, idempotent release, shutdown |
248
249
  | `guarded-fetch.test.ts` | the guarded transport — construction, request building, destination confinement, and response handling |
249
250
  | `guest-api-contract.test.ts` | the shared guest contract on the Dynamic Worker, including caught call, typed inline describe recovery, discovery, utility, batch, and budget failure codes; plus the real authority boundary — local `data:` fetch, denied egress, unresolved DNS, empty environment paths, unavailable filesystem/HTTP builtins, and present runtime globals |
251
+ | `identity-scope.test.ts` | identity-derived connector visibility, personal credential isolation, shared-auth operator control, and personal OAuth callback ownership |
250
252
  | `linear-provider.test.ts` | the Linear proxy's construction, guide, plan-aware catalog superset, and current workspace, template, and issue-sharing classifications |
251
253
  | `meta-tools-call.test.ts` | registry-backed calls: structured errors, truncation and `get_result`, per-connector result bounds, JSON representation failures, MCP content bounds, and offset alignment |
252
254
  | `meta-tools-search.test.ts` | registry-backed discovery: bounded search with page and address maxima, compact and JSON schemas with constraints, typed describe recovery and suggestions, and structured-result compatibility |
@@ -280,7 +282,7 @@ justification for *not* re-running it in workerd, so "it was easier" is not one.
280
282
 
281
283
  | Suite | Covers | Why Node |
282
284
  | --- | --- | --- |
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 |
285
+ | `deployment-shapes.test.ts` | the Worker as the only example with a loader-only sandbox, its agent instructions and setup guide pinning Claude and both ChatGPT Managed OAuth callback forms, 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 |
284
286
  | `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 |
285
287
  | `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 |
286
288
  | `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 |
@@ -5,10 +5,10 @@ the authentication material behind it. It is a small Preact app compiled by the
5
5
  repository's own esbuild step and inlined into a data-free server shell.
6
6
 
7
7
  Read [`ethos.md`](../ethos.md) first. The boundary this subsystem lives inside
8
- is the operator row in its decisions table: **operator routes may manage
9
- authentication material for capabilities declared in deployment configuration,
10
- and may not change the connector set, the tool catalog or annotations, requested
11
- OAuth scopes, admission policy, authorization rules, or caller tool scope.**
8
+ is the human-management invariant: **members may manage authentication material
9
+ for every connector their code-derived view includes, operators may also manage
10
+ deployment tokens and global activity, and neither may change the connector set, tool catalog,
11
+ annotations, requested OAuth scopes, admission policy, or identity rules.**
12
12
  `test/operator-boundary.test.ts` proves it after every mutation route.
13
13
 
14
14
  Both deployment shapes ship the whole feature set behind it, because pages for
@@ -52,6 +52,15 @@ server reads the resulting runtime identity. Sign out navigates to
52
52
  `/cdn-cgi/access/logout`. Mutations still require an exact same-origin
53
53
  `Origin`; an ambient cookie does not weaken the CSRF boundary.
54
54
 
55
+ The shell is shared by members and operators. `/ui/data` uses the same
56
+ identity-scoped registry view as `/mcp`, so it cannot list a connector the
57
+ current caller cannot discover. A member sees credential and OAuth controls for
58
+ every visible connector. Personal actions resolve to that member's principal
59
+ partition; shared actions change the deployment-wide grant. The access-token
60
+ and global activity pages require `identity.operatorAccess`. Existing
61
+ deployments that omit that resolver keep every interactive human as an
62
+ operator.
63
+
55
64
  This runtime selection is the Clerk migration seam. A deployment may contain
56
65
  both providers: before Worker-level Access is attached, the data-free shell
57
66
  selects Clerk; after Access supplies `ctx.access`, it selects ambient auth. That
@@ -49,7 +49,8 @@ which is also why `/health` always has a code-admission shape to report.
49
49
 
50
50
  The request pool is global FIFO across identities. It is a capacity boundary,
51
51
  not tenant fairness: one busy caller can occupy it. Per-tenant fairness needs a
52
- policy above connecta, and one deployment serves one audience anyway
52
+ policy above connecta, and one deployment still serves one tenant even when
53
+ identity rules give its principals different connector views
53
54
  ([`ethos.md`](../ethos.md)), so a global queue is not pretending to supply
54
55
  something it does not.
55
56
 
@@ -6,7 +6,7 @@ token is an independent record rather than one shared, race-prone manifest.
6
6
  The built-in memory and file adapters implement it, as does the Cloudflare KV
7
7
  example.
8
8
 
9
- Connectors may declare an operator-managed `credential` slot. When
9
+ Connectors may declare a human-managed `credential` slot. When
10
10
  `credentials.encryptionKey` is configured, Connecta encrypts values in the
11
11
  deployment storage and exposes read-only access only through that connector's
12
12
  `ctx.credential`. Values, masked values, call arguments, and raw errors never
@@ -27,13 +27,40 @@ Credential mutation is intentionally narrower than MCP access:
27
27
 
28
28
  - a static bearer may call tools and receive the operator handoff, but it
29
29
  cannot write credentials;
30
- - only an admitted Clerk user may use the same-origin credential mutation
31
- routes; and
30
+ - an admitted interactive human may mutate credentials for every visible
31
+ connector: their own partition for personal auth, or the deployment-wide
32
+ value for shared auth; and
32
33
  - saving, replacing, testing, or removing a value never returns that value.
33
34
 
34
- The vault is read for each call. Once an operator saves a replacement,
35
+ The vault is read for each call. Once a signed-in human saves a replacement,
35
36
  the agent can retry immediately without restarting or redeploying Connecta.
36
37
 
38
+ ## Shared and personal auth
39
+
40
+ Connector auth defaults to `authScope: "shared"`. Its credential, OAuth state,
41
+ tokens, catalog cache, and connector storage belong to the deployment. Set
42
+ `authScope: "personal"` when every human principal needs a separate downstream
43
+ account:
44
+
45
+ ```ts
46
+ remoteMcp("linear", {
47
+ url: "https://mcp.linear.app/mcp",
48
+ authScope: "personal",
49
+ auth: { type: "oauth" },
50
+ });
51
+ ```
52
+
53
+ Personal connectors disappear from a request that has no stable human
54
+ principal. For a principal that can see one, connecta partitions connector
55
+ storage, encrypted vault records, catalog caches, OAuth generations, and
56
+ observed result shapes under an opaque SHA-256 identity key. Results used by
57
+ `get_result` are partitioned by the authenticated subject, so one token cannot
58
+ page another token's call even when both tokens belong to the same principal.
59
+
60
+ Literal `auth: { type: "headers" }` cannot be personal because its secret lives
61
+ in deployment code. `remoteMcp()` refuses that combination at construction.
62
+ Use operator-managed credential auth or OAuth instead.
63
+
37
64
  ## A remote MCP connector's static credential
38
65
 
39
66
  `remoteMcp()` accepts a third auth shape beside OAuth and literal headers:
@@ -97,6 +124,17 @@ Registration and token envelopes are bound to the validated authorization
97
124
  server `issuer`. An unbound pre-0.9 envelope is upgraded in place on its first
98
125
  issuer-aware read, preserving the existing grant.
99
126
 
127
+ For personal OAuth, the authorization handoff also stores a 15-minute mapping
128
+ from a SHA-256 digest of `state` to the principal partition. The public callback
129
+ uses that mapping before it verifies state or exchanges the code. Neither the
130
+ browser nor a callback parameter can select a principal. The callback deletes
131
+ the mapping before it exchanges the code, so a second callback cannot replay
132
+ the principal handoff in strongly consistent storage. Cloudflare KV deletion
133
+ is eventually consistent, so handoff consumption there is best-effort across
134
+ PoPs; the downstream authorization code remains single-use. If the callback
135
+ request also carries an interactive identity, Connecta refuses it when that
136
+ principal did not start the flow.
137
+
100
138
  If later discovery resolves a different issuer, Connecta does not send the old
101
139
  client identifier or tokens to it. The provider publishes a new generation
102
140
  epoch, makes every older credential namespace unreadable, cleans up the retired
@@ -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.21.2 | 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.22.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.21.2
109
+ npm pkg set dependencies.@zackbart/connecta=0.22.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.21.2 init current)
133
+ (cd "$SCRATCH" && npx @zackbart/connecta@0.22.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.21.2 init current)` — there is no
189
+ `(cd "$SCRATCH" && npx @zackbart/connecta@0.22.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,22 @@ 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.21.2 → 0.22.0
211
+
212
+ Connector and user policy remain config-as-code. If `identity.connectorAccess`
213
+ is configured, every interactive human may now manage the authentication of
214
+ each connector that resolver makes visible. A personal connector changes only
215
+ that principal's partition; a shared connector changes the deployment-wide
216
+ grant. Keep shared connectors out of a member's view, or change them to
217
+ `authScope: "personal"`, when that member must not rotate the shared grant.
218
+ `identity.operatorAccess` continues to govern deployment access tokens and
219
+ global activity.
220
+
221
+ Paged results also move under the authenticated subject's storage partition.
222
+ Finish any important in-flight `get_result` sequence before upgrading; its old
223
+ result id is not readable from the new partition after deployment. No persisted
224
+ connector catalog or credential migration is required.
225
+
210
226
  ### 0.20.0 → 0.21.2
211
227
 
212
228
  0.21.2 adds no deployment migration beyond 0.21.0. The boundary is additive
@@ -244,6 +260,21 @@ For a Worker currently using Clerk, keep rollback live through the cutover:
244
260
  tag>" }`, not a hostname application for the `workers.dev` URL: the latter
245
261
  gates traffic but does not provide `ctx.access`. Create an Access service
246
262
  token and a **Service Auth** policy for doctor and fully unattended clients.
263
+ In the application's Managed OAuth settings, enable Dynamic Client
264
+ Registration and add these three **Allowed redirect URIs**:
265
+
266
+ ```text
267
+ https://claude.ai/api/mcp/auth_callback
268
+ https://chatgpt.com/connector_platform_oauth_redirect
269
+ https://chatgpt.com/connector/oauth/*
270
+ ```
271
+
272
+ They map to
273
+ `oauth_configuration.dynamic_client_registration.allowed_uris` in the
274
+ Access API, not to the identity policy. The two ChatGPT entries cover its
275
+ stable and callback-id forms. An empty list fails client registration only
276
+ after discovery, so do not treat a working `/.well-known/*` response as
277
+ proof that this step is complete.
247
278
  Do not create a bypass for `/.well-known/*`; Managed OAuth owns that
248
279
  discovery surface.
249
280
 
package/ethos.md CHANGED
@@ -8,8 +8,8 @@ preserve. A contradiction needs a design decision, not a drive-by edit.
8
8
  - **One MCP endpoint, one programmable surface.** Every integration you chose
9
9
  sits behind a capability catalog that agents reach by writing JavaScript,
10
10
  ringed by a few explicit tools for the boundaries code must not cross.
11
- - **A deployment is a small config-as-code file.** Changing what agents can
12
- reach is an edit and a redeploy. One deployment, one tenant, one audience.
11
+ - **A deployment is config-as-code.** One tenant and connector set; principals
12
+ receive config-derived views.
13
13
  - **Curated when available, open when not.** Prefer a maintained prebuilt
14
14
  connection; `remoteMcp()` and `api()` stay first-class for everything else.
15
15
  Every path yields the same `Connector` with the same rules.
@@ -30,8 +30,8 @@ preserve. A contradiction needs a design decision, not a drive-by edit.
30
30
  - **Not a platform.** No runtime registration, admin-editable capability,
31
31
  policy engine, approvals, or pauses.
32
32
  - **Not a schema ingester.** No OpenAPI or GraphQL → tools.
33
- - **Not multi-tenant.** No account model or per-user credential store; scope
34
- stays connector-level, and ambiguity stops rather than guesses.
33
+ - **Not multi-tenant.** No accounts, groups, or sessions. Inbound auth owns
34
+ identity; personal state stays within one tenant.
35
35
  - **Not stateful.** No protocol sessions, no server push; scope resolves per
36
36
  request.
37
37
  - **Not a nanny.** Credentials fail loudly at use; nothing probes one.
@@ -47,7 +47,7 @@ CHANGELOG, not here.
47
47
  | Decision | Verdict | Why |
48
48
  | --- | --- | --- |
49
49
  | OpenAPI / GraphQL ingestion | refused | the disease is a tool nobody chose — a document authored it; hand-written literals, even through a shared factory, are still authorship |
50
- | Multi-tenancy / account model | refused | one deployment per tenant; deploy again |
50
+ | Multi-tenancy / account model | refused | one deployment per tenant; inbound auth owns identity |
51
51
  | Policy engine, approvals, pauses | refused | the host asks the human; connecta only annotates |
52
52
  | Runtime connector registration | refused | config-as-code is the security model |
53
53
  | Provider registry / marketplace | refused | prebuilt connections are imports; discovery happens in docs ([#297](https://github.com/zackbart/connecta/issues/297)) |
@@ -67,7 +67,7 @@ CHANGELOG, not here.
67
67
  | Legacy embedded `UIResource` delivery | refused | superseded upstream, rendered by no client we face ([#266](https://github.com/zackbart/connecta/issues/266)) |
68
68
  | Effect as the core effect system | refused | −4% of the core for +75 KB gzip and a second async paradigm; re-measure at v4 stable ([#470](https://github.com/zackbart/connecta/issues/470)) |
69
69
  | Shared bounded queue under both admission controllers | refused | built and measured −17 lines for a hook-parameterised abstraction ([#453](https://github.com/zackbart/connecta/issues/453)) |
70
- | Toolkits (scoped views) | removed | deploy per audience ([#178](https://github.com/zackbart/connecta/issues/178)) |
70
+ | Caller-selected toolkits | removed | only config may derive an identity's connector view ([#178](https://github.com/zackbart/connecta/issues/178)) |
71
71
  | Proactive credential liveness | removed | fail-at-use is enough ([#179](https://github.com/zackbart/connecta/issues/179)) |
72
72
  | Classic (executor-free) surface | removed | an executor is mandatory ([#273](https://github.com/zackbart/connecta/issues/273)) |
73
73
  | Per-result lexical query coverage | removed | did not earn its response bytes in a precommitted gate ([#323](https://github.com/zackbart/connecta/issues/323)) |
@@ -91,10 +91,10 @@ Breaking one is a design change wearing a disguise.
91
91
  - **A downstream catalog is complete or it is a failure.** A partial catalog is never cached, persisted, or served.
92
92
  - **Activity is payload-free by construction.** The event type has nowhere to put arguments, results, code, or raw errors.
93
93
  - **An observed shape is never a declaration.** Names and broad types only, labeled, and gone behind any declared schema.
94
- - **Credentials never leave the host.** Encrypted at rest, readable only by the owning connector, rendered by nothing.
94
+ - **Credentials never leave the host.** Encrypted at rest, readable only by the owning connector and, for personal auth, its owning principal; rendered by nothing.
95
95
  - **Import-graph purity.** Nothing reachable from the root entry imports a `node:` builtin.
96
96
  - **The published surface is a boundary.** Heavyweight or platform-bound code goes behind an optional-peer subpath.
97
- - **Operator routes manage authentication material, never declared capability.** A downstream catalog is discovered, not declared.
97
+ - **Human routes manage auth, never capability.** Signed-in humans manage auth for visible connectors; operators also manage tokens and global activity.
98
98
  - **Structural mistakes throw at construction.** Booting into the wrong shape is worse than not booting.
99
99
 
100
100
  Connecta began as a radical simplification of
@@ -0,0 +1,44 @@
1
+ # Working on this Connecta Worker deployment
2
+
3
+ This repository is deployment configuration, not a copy of Connecta itself.
4
+
5
+ - Edit `src/index.ts` for connectors, authentication, storage, and public URL.
6
+ - Keep `cloudflareAccessAuth()` as the inbound auth provider. Cloudflare Access
7
+ authenticates the request before the Worker runs; do not add JWT parsing or a
8
+ second Worker-side identity gate.
9
+ - Attach Access to the Worker itself, not only its hostname. Enable Managed
10
+ OAuth and Dynamic Client Registration on that Access application.
11
+ - Managed OAuth's **Allowed redirect URIs** must contain all three entries
12
+ below. This is application configuration under
13
+ `oauth_configuration.dynamic_client_registration.allowed_uris`, not an
14
+ Access Allow policy:
15
+
16
+ ```text
17
+ https://claude.ai/api/mcp/auth_callback
18
+ https://chatgpt.com/connector_platform_oauth_redirect
19
+ https://chatgpt.com/connector/oauth/*
20
+ ```
21
+
22
+ The first is Claude's hosted MCP callback. The two ChatGPT entries cover its
23
+ stable callback and its callback-id form. An empty allowlist lets Access
24
+ discovery work but makes client registration fail with `redirect_uri` not
25
+ allowed. If a client presents a different callback, copy that exact URI from
26
+ its registration attempt and add the narrowest matching entry rather than
27
+ broadening the allowlist to an entire origin.
28
+ - Keep `new DynamicWorkerExecutor({ loader: env.LOADER })` loader-only. Do not
29
+ add bindings, modules, or outbound access to generated code.
30
+ - Keep credentials in Worker secrets. Never commit credential values, Access
31
+ service-token secrets, or `CREDENTIAL_ENCRYPTION_KEY`.
32
+ - Add application logic only inside deliberate `api()` connector handlers.
33
+ Do not copy or modify Connecta package internals here.
34
+ - Prefer `api()` when the agent must see an exact reviewed capability set;
35
+ `remoteMcp()` follows the downstream server's evolving tool catalog.
36
+ - Use Access service credentials for `connecta doctor` and unattended clients.
37
+ A `cta_` token or static Connecta bearer cannot cross the Access edge alone.
38
+ - Run the repository's `npm run check:examples` after configuration changes.
39
+ After deployment, connect both Claude and ChatGPT to `<PUBLIC_URL>/mcp` and
40
+ complete their browser authorization flows before calling setup complete.
41
+
42
+ Do not add alternate entrypoints, policy layers, generated connector catalogs,
43
+ or runtime connector registration. Keep the deployment small enough to review
44
+ as configuration.
@@ -46,15 +46,56 @@ wrangler deploy
46
46
  Cloudflare Access to the Worker itself (the API destination type is `worker`,
47
47
  not a hostname application) and choose the account, email-domain, or
48
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.
49
+ that Access application for interactive MCP clients, turn on Dynamic Client
50
+ Registration, and add these three entries under **Allowed redirect URIs**:
51
+
52
+ ```text
53
+ https://claude.ai/api/mcp/auth_callback
54
+ https://chatgpt.com/connector_platform_oauth_redirect
55
+ https://chatgpt.com/connector/oauth/*
56
+ ```
57
+
58
+ The Claude entry is its fixed hosted-MCP callback. ChatGPT may register either
59
+ its stable callback or a callback-id URL, so both forms are intentional. These
60
+ are Managed OAuth application settings, represented by
61
+ `oauth_configuration.dynamic_client_registration.allowed_uris` in the Access
62
+ API; they do not belong in the Access Allow policy that decides who may sign
63
+ in. Leaving the list empty is a footgun: discovery still works, then Dynamic
64
+ Client Registration fails because the callback is not allowed. If either
65
+ client presents a new redirect URI, copy that exact value from the registration
66
+ attempt and add the narrowest matching entry rather than allowing its entire
67
+ origin.
68
+
69
+ Access then serves OAuth discovery and turns the client's opaque token into the
70
+ trusted `ctx.access` identity connecta reads. A cron job or CI client uses an
71
+ Access service token instead.
72
+
73
+ Through the API, the relevant part of the application is:
74
+
75
+ ```json
76
+ {
77
+ "oauth_configuration": {
78
+ "enabled": true,
79
+ "dynamic_client_registration": {
80
+ "enabled": true,
81
+ "allowed_uris": [
82
+ "https://claude.ai/api/mcp/auth_callback",
83
+ "https://chatgpt.com/connector_platform_oauth_redirect",
84
+ "https://chatgpt.com/connector/oauth/*"
85
+ ]
86
+ }
87
+ }
88
+ }
89
+ ```
53
90
 
54
91
  Cloudflare's [Worker Access guide](https://developers.cloudflare.com/workers/configuration/cloudflare-access/)
55
92
  owns the dashboard/API steps; its [Managed OAuth guide](https://developers.cloudflare.com/cloudflare-one/access-controls/applications/http-apps/managed-oauth/)
56
93
  owns client registration, redirect allowlists, and token lifetimes.
57
94
 
95
+ [`AGENTS.md`](./AGENTS.md) repeats the callback invariant for coding agents
96
+ working in a copied deployment. Do not remove the entries there when changing
97
+ the Access policy or application.
98
+
58
99
  The checked-in `access.dev` block gives `wrangler dev` a local operator
59
100
  identity. Remove the block to test the missing-Access refusal. It has no effect
60
101
  on a deployed Worker's production identity.
@@ -75,13 +116,9 @@ connecta's manifest but never installed with it, and published as
75
116
  one outside and npm says so at install time instead of leaving a Worker to
76
117
  discover the skew in production ([#376](https://github.com/zackbart/connecta/issues/376)).
77
118
 
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
- ```
119
+ `cloudflareAccessAuth()` has no dependency of its own. This Worker example has
120
+ no Clerk import, secret, package, or fallback provider. Docker deployments keep
121
+ the Clerk path in the Node template.
85
122
 
86
123
  Then point an MCP client at `<PUBLIC_URL>/mcp`, and open `<PUBLIC_URL>/` for
87
124
  Connections. Credentials is at `/credentials`, named MCP access tokens are at
@@ -96,9 +133,21 @@ as deployed; the fourth needs a database, so it is commented in place.
96
133
 
97
134
  **Operator sign-in** is the `cloudflareAccessAuth()` entry in `src/index.ts`.
98
135
  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.
136
+ MCP and human-management pages; a service-token identity can use MCP but cannot
137
+ write a credential, run downstream OAuth, or issue a connecta token. Cloudflare
138
+ still owns the outer application admission policy, but Connecta's user roster,
139
+ connector access, and deployment roles stay in `src/index.ts`.
140
+
141
+ **Several users** need no second auth system or Connecta account dashboard.
142
+ Uncomment the `identity` block in `src/index.ts` to derive connector ids and
143
+ deployment-operator membership from the Access principal. Connectors remain
144
+ visible to everyone and every human remains an operator when that block is
145
+ absent. A signed-in human may edit auth for every connector their view includes.
146
+ Add `authScope: "personal"` when each user should connect a different downstream
147
+ account; leave it shared only when any user with connector access may rotate the
148
+ deployment-wide grant. Static headers stay shared because their value lives in
149
+ deployment configuration. See [inbound identity](../../documentation/auth.md#principals-visibility-and-operators)
150
+ for the resolver contract.
102
151
 
103
152
  **The credential vault** is `credentials: { encryptionKey: … }`, backed by the
104
153
  same KV namespace as everything else and encrypted with the
@@ -17,15 +17,17 @@
17
17
  * 1. `npm install` in the connecta package root (../../ from here) so the
18
18
  * package import and wrangler resolve. A copy in its own repository
19
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.
20
+ * an optional peer.
22
21
  * 2. Create a KV namespace and put its id in wrangler.jsonc under `kv_namespaces`.
23
22
  * 3. Set secrets:
24
23
  * wrangler secret put DOWNSTREAM_TOKEN
25
24
  * wrangler secret put CREDENTIAL_ENCRYPTION_KEY
26
25
  * 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.
26
+ * 4. Attach Cloudflare Access to this Worker. Enable Managed OAuth and
27
+ * Dynamic Client Registration. Its Allowed redirect URIs must include
28
+ * Claude's https://claude.ai/api/mcp/auth_callback plus ChatGPT's
29
+ * https://chatgpt.com/connector_platform_oauth_redirect and
30
+ * https://chatgpt.com/connector/oauth/* forms (see ../AGENTS.md).
29
31
  * 5. Use the Workers Paid plan required by the `worker_loaders` binding.
30
32
  * 6. `wrangler deploy` from this folder (examples/worker), where wrangler.jsonc
31
33
  * lives. Point your MCP client at `<PUBLIC_URL>/mcp`.
@@ -37,17 +39,12 @@ import {
37
39
  remoteMcp,
38
40
  } from "@zackbart/connecta";
39
41
  import { cloudflareAccessAuth } from "@zackbart/connecta/auth/cloudflare-access";
40
- // Rollback for a deployment migrating from Clerk:
41
- // import { clerkAuth } from "@zackbart/connecta/auth/clerk";
42
42
  import { cloudflareKvStorage } from "./cloudflare-kv.js";
43
43
  // Activity history, off by default because it needs a D1 database.
44
44
  // import { d1ActivityStore } from "./d1-activity.js";
45
45
 
46
46
  interface Env {
47
47
  CONNECTA_KV: KVNamespace;
48
- // Keep these during a Clerk migration until Access has been verified:
49
- // CLERK_PUBLISHABLE_KEY: string;
50
- // CLERK_SECRET_KEY: string;
51
48
  /**
52
49
  * Base64 32-byte AES key encrypting operator-managed credentials in KV.
53
50
  * Unset means no vault: /credentials stays read-only and connecta says so at
@@ -75,20 +72,24 @@ function build(env: Env) {
75
72
  // operator pages; a service token may use MCP but cannot mutate operator
76
73
  // state. Neither path asks connecta to parse a JWT.
77
74
  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
- // }),
87
75
  ],
88
- // Connectors that declare a `credential` slot become editable at
89
- // /credentials, encrypted with this key before anything reaches KV. A
90
- // saved replacement takes effect on the next call no redeploy, and no
91
- // liveness probe: credentials fail at use.
76
+ // Optional code-owned roster. Access proves the identity; connecta derives
77
+ // connector visibility and deployment-operator status from the stable id
78
+ // it supplies. A signed-in human may manage auth for every connector this
79
+ // view includes. Omit the block to keep every connector visible and every
80
+ // human a deployment operator.
81
+ // identity: {
82
+ // connectorAccess: ({ principal }) =>
83
+ // principal?.id === "ACCESS_USER_UUID"
84
+ // ? ["notion", "echo"]
85
+ // : ["echo"],
86
+ // operatorAccess: ({ id }) => id === "ACCESS_USER_UUID",
87
+ // },
88
+ // Connectors that declare a `credential` slot become editable by every
89
+ // signed-in human who can see that connector at /credentials, encrypted
90
+ // with this key before anything reaches KV. A saved replacement takes
91
+ // effect on the next call — no redeploy, and no liveness probe:
92
+ // credentials fail at use.
92
93
  //
93
94
  // The key is the vault, not the page: /credentials is a list of connector
94
95
  // slots, so it stays hidden until a connector declares one. Neither
@@ -123,6 +124,9 @@ function build(env: Env) {
123
124
  // type: "credential",
124
125
  // credential: { label: "Notion internal integration token" },
125
126
  },
127
+ // Use `authScope: "personal"` with OAuth or credential auth when each
128
+ // Access user connects their own downstream account. Literal headers
129
+ // are deployment-owned and cannot be personal.
126
130
  }),
127
131
  api("echo", {
128
132
  description: "Echo — text transforms",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zackbart/connecta",
3
- "version": "0.21.2",
3
+ "version": "0.22.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.",
@@ -68,6 +68,13 @@ import, the two `process.env.CLERK_*` reads, and the `clerkAuth({ … })` entry
68
68
  Applications → DCR) if MCP clients should sign in through it too, and set
69
69
  `PUBLIC_URL` first — Clerk redirects back to it.
70
70
 
71
+ Clerk remains the identity provider when several people share this Docker
72
+ deployment. Uncomment the `identity` block in `src/index.ts` to give each Clerk
73
+ principal a config-derived connector view and to choose operators. Add
74
+ `authScope: "personal"` to a connector when each person should supply their own
75
+ credential or finish their own downstream OAuth flow. Without those options,
76
+ all connectors and auth stay shared exactly as before.
77
+
71
78
  **2. Credential vault.** Uncomment `credentials` and set
72
79
  `CONNECTA_CREDENTIAL_KEY` to a base64 32-byte AES key:
73
80
 
@@ -15,7 +15,7 @@
15
15
  "typecheck": "tsc --noEmit"
16
16
  },
17
17
  "dependencies": {
18
- "@zackbart/connecta": "0.21.2",
18
+ "@zackbart/connecta": "0.22.0",
19
19
  "quickjs-emscripten": "0.32.0"
20
20
  },
21
21
  "devDependencies": {
@@ -42,10 +42,10 @@ const stateFile = process.env.CONNECTA_STATE_FILE || "./.connecta-state.json";
42
42
  const publicUrl = process.env.PUBLIC_URL || `http://localhost:${port}`;
43
43
 
44
44
  // Operator sign-in. A bearer token is a client key: it may call tools and read
45
- // connector status, but only a Clerk-authenticated operator may write a
46
- // credential or issue an access token. Without this block the operator pages
47
- // still render — an operator pastes the bearer to read them — and Credentials
48
- // and Tokens stay read-only.
45
+ // connector status, but only a Clerk-authenticated human may write a visible
46
+ // connector's credential, and only an operator may issue an access token.
47
+ // Without this block the operator pages still render — an operator pastes the
48
+ // bearer to read them — and Credentials and Tokens stay read-only.
49
49
  // const clerkPublishableKey = process.env.CLERK_PUBLISHABLE_KEY;
50
50
  // const clerkSecretKey = process.env.CLERK_SECRET_KEY;
51
51
  // if (!clerkPublishableKey || !clerkSecretKey) {
@@ -67,6 +67,15 @@ const connecta = createConnecta({
67
67
  // // allowedDomains: ["acme.com"],
68
68
  // }),
69
69
  ],
70
+ // Optional member/operator split for Clerk-backed Docker deployments.
71
+ // Connector access is derived from the authenticated identity and cannot be
72
+ // selected by an MCP argument. Omit this block for the legacy all-visible,
73
+ // all-interactive-users-are-operators behavior.
74
+ // identity: {
75
+ // connectorAccess: ({ principal }) =>
76
+ // principal?.id === "user_admin" ? "all" : ["time"],
77
+ // operatorAccess: ({ id }) => id === "user_admin",
78
+ // },
70
79
  publicUrl,
71
80
  // Required: model-written programs run in a bounded QuickJS child.
72
81
  executor: quickJsExecutor(),