@zackbart/connecta 0.23.0 → 0.24.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 (78) hide show
  1. package/AGENTS.md +5 -0
  2. package/CHANGELOG.md +40 -0
  3. package/README.md +18 -10
  4. package/dist/activity-friction.d.ts +3 -0
  5. package/dist/activity-friction.js +19 -0
  6. package/dist/activity.d.ts +11 -2
  7. package/dist/activity.js +15 -19
  8. package/dist/auth/downstream-oauth.d.ts +2 -1
  9. package/dist/auth/downstream-oauth.js +10 -1
  10. package/dist/branding.d.ts +67 -0
  11. package/dist/branding.js +176 -0
  12. package/dist/connectors/remote-mcp.js +3 -5
  13. package/dist/credential-contract.d.ts +24 -0
  14. package/dist/credential-contract.js +1 -0
  15. package/dist/credential-rules.d.ts +85 -0
  16. package/dist/credential-rules.js +107 -0
  17. package/dist/credentials.d.ts +4 -100
  18. package/dist/credentials.js +3 -107
  19. package/dist/index.d.ts +22 -55
  20. package/dist/index.js +30 -58
  21. package/dist/invocation.js +2 -3
  22. package/dist/meta-tools.d.ts +4 -0
  23. package/dist/meta-tools.js +8 -4
  24. package/dist/module-contracts.d.ts +19 -0
  25. package/dist/module-contracts.js +1 -0
  26. package/dist/operator-ui/generated.js +2 -2
  27. package/dist/operator-ui/model.d.ts +6 -3
  28. package/dist/operator-ui/view.d.ts +2 -18
  29. package/dist/operator-ui/view.js +3 -20
  30. package/dist/registry.d.ts +4 -1
  31. package/dist/registry.js +4 -6
  32. package/dist/routes/activity.js +1 -1
  33. package/dist/routes/credentials.js +5 -2
  34. package/dist/routes/mcp.js +7 -3
  35. package/dist/routes/oauth-management.d.ts +2 -0
  36. package/dist/routes/oauth-management.js +108 -0
  37. package/dist/routes/oauth.d.ts +0 -1
  38. package/dist/routes/oauth.js +21 -121
  39. package/dist/routes/shared.d.ts +19 -17
  40. package/dist/routes/shared.js +48 -44
  41. package/dist/routes/ui.js +36 -33
  42. package/dist/server.js +6 -26
  43. package/dist/types.d.ts +2 -0
  44. package/dist/ui.d.ts +15 -70
  45. package/dist/ui.js +176 -317
  46. package/dist/version.d.ts +1 -1
  47. package/dist/version.js +1 -1
  48. package/documentation/architecture.md +26 -17
  49. package/documentation/auth.md +61 -106
  50. package/documentation/cloudflare.md +1 -1
  51. package/documentation/connectors.md +1 -1
  52. package/documentation/linear.md +1 -1
  53. package/documentation/meta-tools.md +6 -4
  54. package/documentation/mixpanel.md +1 -1
  55. package/documentation/notion.md +2 -2
  56. package/documentation/operations.md +12 -14
  57. package/documentation/operator-ui.md +82 -104
  58. package/documentation/optional-modules-upgrade.md +243 -0
  59. package/documentation/provider-conventions.md +5 -3
  60. package/documentation/revenuecat.md +1 -1
  61. package/documentation/storage-and-credentials.md +59 -40
  62. package/documentation/stripe.md +1 -1
  63. package/documentation/upgrading.md +29 -4
  64. package/ethos.md +22 -30
  65. package/examples/worker/AGENTS.md +3 -1
  66. package/examples/worker/README.md +68 -84
  67. package/examples/worker/src/d1-activity.ts +1 -1
  68. package/examples/worker/src/index.ts +11 -6
  69. package/package.json +18 -2
  70. package/templates/node/AGENTS.md +8 -6
  71. package/templates/node/README.md +56 -67
  72. package/templates/node/package.json +1 -1
  73. package/templates/node/src/file-activity.ts +1 -1
  74. package/templates/node/src/index.ts +11 -12
  75. package/dist/access-tokens.d.ts +0 -31
  76. package/dist/access-tokens.js +0 -236
  77. package/dist/routes/access-tokens.d.ts +0 -6
  78. package/dist/routes/access-tokens.js +0 -83
@@ -1,67 +1,60 @@
1
1
  # Inbound auth
2
2
 
3
- Inbound auth decides who may reach the MCP endpoint. A deployment may admit a
4
- static bearer, operator-issued access tokens, Clerk identities, Cloudflare
5
- Access identities on Workers, or a mixture. Static bearers are checked first;
6
- the remaining providers keep configuration order. The first successful
7
- identity owns the activity actor for that request.
3
+ Inbound auth decides who may reach the MCP endpoint. Import configured bearer
4
+ support from `@zackbart/connecta/auth/bearer`, Clerk from `/auth/clerk`, or
5
+ Cloudflare Access from `/auth/cloudflare-access`. Providers may be combined;
6
+ static bearers are checked first, then other providers in configuration order.
7
+ Connecta no longer issues `cta_` tokens or serves token-management routes.
8
8
 
9
9
  ## Principals, visibility, and operators
10
10
 
11
- Connecta distinguishes three identities. The actor is the exact caller written
12
- to activity. The subject is any stable authenticated caller and owns transient
13
- results such as `get_result` pages. The principal is the human owner of personal
11
+ The actor identifies the caller in activity. The subject owns transient results
12
+ such as `get_result` pages. The principal is the human owner of personal
14
13
  connector auth. An interactive Clerk or Access user supplies all three. A
15
- Cloudflare service identity has an actor and subject but no principal. A
16
- connecta access token has its own actor and subject and inherits the principal
17
- that created it, so agents using that token reach the creator's personal
18
- connections without becoming operators.
19
-
20
- `identity.connectorAccess` derives the connector ids a caller may discover and
21
- invoke. The resolver receives authenticated identity data, never request input,
22
- and returns `"all"` or a list of ids declared in `connectors`. An unknown id or
23
- a thrown resolver fails the request closed.
24
-
25
- `identity.connectorAccess` is also the credential-management boundary. A
26
- signed-in human may save, test, disconnect, and authorize every visible
27
- connector: personal auth changes only that principal's partition, while shared
28
- auth changes the deployment-wide grant for everyone who can see the connector.
29
- Use `authScope: "personal"` when one member must not rotate another member's
30
- connection.
31
-
32
- `identity.operatorAccess` separately reserves deployment-wide administration:
33
- access-token creation and global activity history. Omit the resolver to
34
- preserve the prior rule that every interactive human is an operator. When it is
35
- configured, activity history is operator-only because its global event stream
36
- contains other principals' connector names and actors.
14
+ Cloudflare service identity has an actor and subject but no principal.
15
+
16
+ `identity.connectorAccess` returns `"all"` or declared connector ids. It governs
17
+ discovery and use, and defaults to all connectors. Visibility alone grants no
18
+ authentication-management permission. Two independent resolvers return
19
+ `"all"`, `"none"`, or declared connector ids:
20
+
21
+ - `credentialAdministration` allows an interactive human to manage shared
22
+ credentials and shared OAuth grants.
23
+ - `personalConnection` allows an interactive human to manage their own
24
+ credentials and OAuth grants on personal connectors.
25
+
26
+ Both default to `"none"`. Each action requires visibility and the relevant
27
+ permission. Personal actions also require a stable namespaced principal and
28
+ always use that principal's partition. Resolver exceptions and unknown ids
29
+ fail closed. Permissions come from authenticated identity, never caller input.
30
+
31
+ The management resolvers receive `Readonly<AuthenticatedIdentity>`.
32
+ `identity.activityAccess` receives `Readonly<IdentityReference>` with `id` and
33
+ `namespace`, and controls reading global activity. Its default admits
34
+ interactive humans, so team deployments should set it explicitly if the event
35
+ stream should be restricted. It replaces `operatorAccess`; there is no general
36
+ administrator role or token-management authority.
37
37
 
38
38
  ```ts
39
39
  createConnecta({
40
40
  auth: cloudflareAccessAuth(),
41
41
  identity: {
42
42
  connectorAccess: ({ principal }) =>
43
- principal?.id === "user_a"
44
- ? ["shared_docs", "personal_linear"]
45
- : ["shared_docs"],
46
- operatorAccess: ({ id }) => id === "user_a",
43
+ principal?.id === "owner-id" ? "all" : ["shared_docs", "personal_linear"],
44
+ credentialAdministration: ({ principal }) =>
45
+ principal?.id === "owner-id" ? "all" : "none",
46
+ personalConnection: () => ["personal_linear"],
47
+ activityAccess: ({ id }) => id === "owner-id",
47
48
  },
48
- connectors: [
49
- remoteMcp("shared_docs", { url: "https://example.com/mcp" }),
50
- remoteMcp("personal_linear", {
51
- url: "https://mcp.linear.app/mcp",
52
- authScope: "personal",
53
- auth: { type: "oauth" },
54
- }),
55
- ],
49
+ connectors,
56
50
  executor,
57
51
  });
58
52
  ```
59
53
 
60
- Identity namespaces matter. Built-in Clerk and Access providers supply one.
61
- A custom interactive provider must set `activityActorNamespace` before its
62
- users can own personal auth. It may still use the legacy operator behavior
63
- without one, but connecta will not merge unnamespaced users into personal
64
- storage.
54
+ Built-in Clerk and Access providers supply identity namespaces. A custom
55
+ interactive provider must set `activityActorNamespace` before its users can
56
+ own personal auth. Keep the namespace and principal ids stable across upgrades;
57
+ changing them selects different personal storage partitions.
65
58
 
66
59
  ## Cloudflare Access on Workers
67
60
 
@@ -92,12 +85,11 @@ also means it is deliberately not a Node or `cloudflared` origin adapter, and
92
85
  it does not survive a Service Binding hop: those shapes need their own explicit
93
86
  trust boundary.
94
87
 
95
- A human identity gets MCP and personal-connection access. It gets operator
96
- access unless `identity.operatorAccess` says otherwise. A Cloudflare service-token
97
- identity gets MCP access and a stable activity subject, but no `userId`, so it
98
- cannot write credentials, run downstream OAuth mutations, or issue connecta
99
- tokens. Access policy decides who reaches the Worker; connecta does not mirror
100
- email domains, groups, or device posture into a second policy layer.
88
+ A human identity gets a code-derived MCP view. Managing connection auth requires
89
+ an explicit `credentialAdministration` or `personalConnection` grant. An Access
90
+ service identity has no human principal and cannot mutate connection auth.
91
+ Access decides admission and identity; Connecta configuration selects connector
92
+ access and these narrower permissions.
101
93
 
102
94
  Protect the Worker with a Worker-level Access application whose destination is
103
95
  `{ "type": "worker", "worker_id": "<the Worker script tag>" }`. A traditional
@@ -135,8 +127,7 @@ Worker-level Access runs before every connecta route. Consequently:
135
127
 
136
128
  - `/health`, operator pages, downstream OAuth callbacks, and `/mcp` all require Access unless a more-specific hostname/path
137
129
  policy says otherwise;
138
- - a static connecta bearer and a `cta_…` token are not standalone edge
139
- credentials, because Cloudflare rejects them before connecta sees them; and
130
+ - a static Connecta bearer is not a standalone edge credential, because Cloudflare rejects them before connecta sees them; and
140
131
  - custom public webhooks belong to the deployment outside Connecta and need
141
132
  their own Access routing policy. Keep Connecta's OAuth discovery paths
142
133
  protected when Managed OAuth is enabled.
@@ -156,57 +147,21 @@ publishable slot, and a startup error is a log line. A deployment that builds
156
147
  per request, as the Workers shape does, sees the same error on its first
157
148
  request instead of a base64 stack on every route.
158
149
 
159
- ## Operator-issued access tokens
150
+ ## Human authentication management
160
151
 
161
- Set `accessTokens: {}` to let eligible interactive operators create named Bearer
162
- tokens at `/tokens`:
152
+ Credential and OAuth mutation require an admitted interactive human, connector
153
+ visibility, the appropriate shared or personal permission, and an exact
154
+ same-origin `Origin` for browser requests. A configured MCP bearer never becomes
155
+ a browser management credential.
163
156
 
164
- ```ts
165
- createConnecta({
166
- storage,
167
- auth: clerkAuth({ /* ... */ }),
168
- accessTokens: {},
169
- connectors,
170
- });
171
- ```
157
+ With `ui: operatorUi()` and a vault, static credential recovery can return a
158
+ secret-free handoff to the connection UI. Without the UI, that recovery is
159
+ `unavailable`; Connecta does not return a link to a missing page. An authorized
160
+ interactive MCP caller can still start downstream OAuth through
161
+ `authorize_connector` without the UI. Core owns the callback and verifies state
162
+ and principal ownership independently of the optional browser application.
172
163
 
173
- The storage adapter must implement `list(prefix)`. Connecta returns each
174
- `cta_…` secret once and stores only its SHA-256 digest plus non-secret metadata.
175
- The operator can rename or revoke a token later. Revocation removes admission
176
- before updating its display metadata, so a partial storage failure fails
177
- secure.
178
-
179
- Each token has an immutable ID. Activity records store that ID and resolve its
180
- current friendly name only while an authorized operator reads activity.
181
- Revoked records remain as metadata tombstones so historical calls keep their
182
- friendly attribution. New tokens also retain the creating principal. Their MCP
183
- requests use that principal's connector visibility and personal auth while the
184
- token itself remains the activity actor and result owner.
185
-
186
- Access tokens authenticate MCP clients; they are never operator credentials.
187
- Creation, rename, and revocation require the same eligible human identity and
188
- same-origin mutation boundary as connector credentials. `maxActive` defaults
189
- to 100 and can be set from 1 through 1,000.
190
-
191
- Issuance and revocation inherit the consistency guarantees of the configured
192
- storage adapter. Use strongly consistent storage when either change must take
193
- effect globally without a convergence window.
194
-
195
- Human credential mutation is a separate, narrower boundary. The
196
- `/credentials` shell contains no secret data before authentication, and the
197
- mutation API requires same-origin requests from an admitted interactive human.
198
- That human may mutate only visible connector slots. An MCP bearer is never
199
- treated as a browser credential, even when it can call every connector.
200
-
201
- This split is visible in recovery:
202
-
203
- - a bearer-authenticated agent may receive `recovery: "operator_config"` and
204
- pass its `operatorUrl` to a human;
205
- - an interactive human with connector access opens that URL, signs in, and updates the
206
- credential; and
207
- - a bearer-only deployment still returns the handoff honestly, but mutation
208
- remains unavailable until interactive user auth is configured.
209
-
210
- See [meta-tools](./meta-tools.md#authorization-recovery) for the stable recovery
211
- envelope and [storage and credentials](./storage-and-credentials.md) for vault
212
- rules.
164
+ See [meta-tools](./meta-tools.md#authorization-recovery) and
165
+ [storage and credentials](./storage-and-credentials.md). The
166
+ [upgrade guide](./upgrading.md#0240-optional-modules) covers moving clients
167
+ off removed Connecta-issued tokens before changing deployment configuration.
@@ -135,7 +135,7 @@ Dashboard and API tabs.
135
135
 
136
136
  `verify_api_token` needs no permission beyond the token existing. In legacy
137
137
  mode, `verify_global_api_key` reads `/user` to confirm the email and key pair.
138
- The `/credentials` Test action runs the matching check before storage.
138
+ The connection UI Test action at `/` runs the matching check before storage.
139
139
 
140
140
  Cloudflare rate-limits *authentication failures* aggressively and separately
141
141
  from the global limit: a few requests with a bad token return HTTP 429 with
@@ -367,7 +367,7 @@ A remote MCP connector that authenticates with a static key has two ways to
367
367
  receive one. `{ type: "headers", headers }` bakes the literal value into the
368
368
  deployment file, which suits a secret the runtime already holds.
369
369
  `{ type: "credential" }` declares the slot instead and lets an operator paste
370
- the key at `/credentials`, where it is encrypted at rest and rotatable without
370
+ the key at the connection UI at `/`, where it is encrypted at rest and rotatable without
371
371
  a redeploy:
372
372
 
373
373
  ```ts
@@ -88,7 +88,7 @@ configuration. A personal API key carries the acting user's full workspace
88
88
  permissions, so pair it with `access: "read-only"` unless the deployment
89
89
  genuinely writes.
90
90
 
91
- The same key can arrive from `/credentials` instead, which is what a deployment
91
+ The same key can arrive from the connection UI at `/` instead, which is what a deployment
92
92
  with no secret store — or an operator who rotates keys without a redeploy —
93
93
  wants:
94
94
 
@@ -316,19 +316,21 @@ Every typed `auth_required` call failure uses the same envelope:
316
316
  handoff:
317
317
 
318
318
  - `oauth`: an `authorizationUrl` and consent instructions;
319
- - `operator_config`: an `operatorUrl` ending in `/credentials`, plus the
319
+ - `operator_config`: an `operatorUrl` to the mounted connection UI, plus the
320
320
  declared credential label and field names/guidance; or
321
321
  - `unavailable`: an honest deployment/configuration message.
322
322
 
323
323
  The class follows what the connector declares, not how it was authored: a
324
324
  `remoteMcp()` connection using `auth: { type: "credential" }` declares a slot
325
- and no OAuth flow, so it lands in `operator_config` beside every `api()`
326
- credential.
325
+ and no OAuth flow, so it uses `operator_config` when both vault and UI are
326
+ configured. Without either it returns `unavailable`, never a dead UI link.
327
327
 
328
328
  The tool accepts no secret. `force` applies only to OAuth and may discard its
329
329
  stored grant before restarting consent. Static credential values are written
330
330
  only through the same-origin interactive-user credential route, and only for a
331
- connector visible to that user. After OAuth consent or a human update, retry
331
+ connector visible to that user with the relevant shared or personal management
332
+ permission. OAuth start, including `force`, requires that permission too. Core
333
+ callbacks work without the UI for authorized interactive callers. After OAuth consent or a human update, retry
332
334
  the original operation; a static update is read from the vault on the next call
333
335
  and needs no redeploy.
334
336
 
@@ -50,7 +50,7 @@ password, not ordinary configuration. Mixpanel currently labels service-account
50
50
  MCP authentication beta. Prefer OAuth unless the deployment is intentionally
51
51
  headless.
52
52
 
53
- The same service account can arrive from `/credentials` instead, and there the
53
+ The same service account can arrive from the connection UI at `/` instead, and there the
54
54
  operator pastes the readable pair rather than an encoded blob:
55
55
 
56
56
  ```ts
@@ -58,7 +58,7 @@ it does not assume the REST interface's endpoint budget describes MCP traffic.
58
58
 
59
59
  One operator-managed credential: an internal integration token from
60
60
  [notion.so/profile/integrations](https://www.notion.so/profile/integrations).
61
- The deployment needs `credentials.encryptionKey` configured, or the token
61
+ The deployment needs `vault: encryptedCredentialVault(storage, encryptionKey)` configured, or the token
62
62
  cannot be stored and every call fails `auth_required` at use.
63
63
 
64
64
  Two Notion-specific facts decide whether a working token is enough:
@@ -69,7 +69,7 @@ Two Notion-specific facts decide whether a working token is enough:
69
69
  default. `list_comments` and `add_comment` fail with 403 until an operator
70
70
  turns them on in Notion.
71
71
 
72
- `/credentials` offers a Test action, which calls `GET /v1/users/me` — the
72
+ The connection UI at `/` offers a Test action, which calls `GET /v1/users/me` — the
73
73
  cheapest call that proves a token is live — and reports the workspace it
74
74
  authenticated into.
75
75
 
@@ -33,8 +33,7 @@ and `test/package-surface.test.ts` fails if the two ever drift apart.
33
33
  There are exactly two deployment shapes.
34
34
  [`templates/node/`](../templates/node/) is what `connecta init` copies — the
35
35
  one standalone Node project, Docker-ready rather than Docker-only — and
36
- [`examples/worker/`](../examples/worker/) is the Cloudflare shape. Both ship
37
- the whole operator feature set; each README walks through its own enablement.
36
+ [`examples/worker/`](../examples/worker/) is the Cloudflare shape. Both show explicit optional modules; each README walks through enablement.
38
37
  A third scaffold that is a diff away from either is the shape
39
38
  [#344](https://github.com/zackbart/connecta/issues/344) deleted, so do not add
40
39
  one.
@@ -84,16 +83,15 @@ optional.
84
83
  | `connectors` | — (required) | the connector set ([connectors](./connectors.md)) |
85
84
  | `executor` | — (required) | the sandbox `execute_code` runs in ([code mode](./code-mode.md#what-an-executor-must-implement)) |
86
85
  | `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)) |
88
- | `storage?` | `memoryStorage()` | the one state seam for catalogs, result paging, credentials, and access tokens ([storage](./storage-and-credentials.md)) |
86
+ | `identity?` | all visible; auth management denied; interactive activity reads | `{ connectorAccess?, credentialAdministration?, personalConnection?, activityAccess? }` derives separate use and management permissions ([identity](./auth.md#principals-visibility-and-operators)) |
87
+ | `storage?` | `memoryStorage()` | connector state, catalogs, and result paging; pass storage explicitly to the optional vault ([storage](./storage-and-credentials.md)) |
89
88
  | `publicUrl?` | per-request origin | public base URL; an HTTPS value also redirects inbound HTTP |
90
- | `logger?` | `console`, prefixed `[connecta]` | `{ debug, info, warn, error }` |
91
- | `branding?` | neutral Connecta defaults | operator-page and OAuth result-page labels and marks |
89
+ | `logger?` | `console`, prefixed `[connecta]` | `{ debug, info, warn, error }`, or `"silent"` to suppress diagnostic output; independent of activity history |
90
+ | `ui?` | unset | `operatorUi({ branding? })` from `/ui`; omitted means no browser UI routes or bundle imports |
92
91
  | `serverInfo?` | `connecta` / package version | `{ name, version, title?, websiteUrl?, icons? }` per the MCP icons spec |
93
92
  | `deploymentInfo?` | unset | arbitrary metadata exposed by `/health` |
94
- | `activity?` | unset | `{ store, readGate?, deploymentId? }` payload-free activity storage, an optional operator-read gate, and a stable event label |
95
- | `credentials.encryptionKey?` | unset | base64 32-byte AES key for the connector vault. Without it, connectors declaring `credential` warn and their slots stay unmanageable |
96
- | `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)) |
93
+ | `activity?` | unset | `activityHistory({ store, readGate?, deploymentId? })` from `/activity`; omitted means no event recording or history reads |
94
+ | `vault?` | unset | `CredentialVault`; `encryptedCredentialVault(storage, encryptionKey)` from `/credentials` supplies the built-in encrypted implementation |
97
95
  | `discovery.concurrency?` | 4 | connector catalogs/status probes in flight at once |
98
96
  | `discovery.catalogTtlSeconds?` | 300 | fresh TTL for cached tool lists |
99
97
  | `discovery.persistCatalog?` | true | persist serializable catalogs as a manifest plus revision-addressed chunks |
@@ -229,7 +227,6 @@ in.
229
227
 
230
228
  | Suite | Covers |
231
229
  | --- | --- |
232
- | `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 |
233
230
  | `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 |
234
231
  | `api-connector.test.ts` | `api()` — kind, description, tool defs, dispatch, default args, unknown tools, handler throws, argument validation, and the construction contract |
235
232
  | `bearer.test.ts` | constant-time bearer compare, case-insensitive scheme, 401 challenges, and the retired audience options refusing rather than silently unbinding |
@@ -252,7 +249,7 @@ in.
252
249
  | `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 |
253
250
  | `guarded-fetch.test.ts` | the guarded transport — construction, request building, destination confinement, and response handling |
254
251
  | `guest-api-contract.test.ts` | the shared guest contract on the Dynamic Worker, including caught call, typed inline describe recovery, discovery, utility, parallel-call, 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 |
255
- | `identity-scope.test.ts` | identity-derived connector visibility, personal credential isolation, shared-auth operator control, and personal OAuth callback ownership |
252
+ | `identity-scope.test.ts` | identity-derived connector visibility, personal credential isolation, separate shared-auth and personal-auth management permissions, and personal OAuth callback ownership |
256
253
  | `linear-provider.test.ts` | the Linear proxy's construction, guide, plan-aware catalog superset, and current workspace, template, and issue-sharing classifications |
257
254
  | `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 |
258
255
  | `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 |
@@ -261,11 +258,12 @@ in.
261
258
  | `notion-provider.test.ts` | Notion's API and MCP construction, the hosted safety manifest and drift behavior, the deliberate REST surface including declined expanded page inputs, request construction, lean projections, both pagination conventions, error mapping, and writes |
262
259
  | `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 |
263
260
  | `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 |
261
+ | `optional-modules.test.ts` | absent modules, UI-free OAuth, fast lists and independent detail deadlines, explicit auth-management grants, invalid-resolver refusal, and passive OAuth consent-state protection |
264
262
  | `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) |
265
263
  | `provider-registry.test.ts` | all seven maintained providers inside real deployments: boot, description, address, catalog, storage, credential, admission, and activity isolation; plus provider-specific discovery and guide contracts |
266
264
  | `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 |
267
265
  | `remote-mcp.test.ts` | `remoteMcp()` against an in-process server through the `_transportFactory` seam: passthrough, downstream `isError`, Workers-safe output-schema validation, request-scoped client reuse and at-most-once scope close; plus the real transport's manual redirect policy, destination guard, credential containment, and downstream session termination |
268
- | `remote-mcp-credential.test.ts` | `remoteMcp()` drawing a static key from `/credentials`: the declared slot and its refusal of named fields and bad header names, header framing (bearer, bare, and the two `Basic` forms) observed on the wire, an empty slot failing as `auth_required` rather than reaching the downstream, a value carrying a control character refused before framing and absent from every surface — `call_tool`, `status`, the Test result, the payload-free activity event, and the thrown error — rotation replacing the cached client and a connect already in flight while a wiped value fails the next call, the Test action's catalog probe and scope close, the cleartext-destination warning, and the vault and `authorize_connector` handoff end to end |
266
+ | `remote-mcp-credential.test.ts` | `remoteMcp()` drawing a static key from the connection UI: the declared slot and its refusal of named fields and bad header names, header framing (bearer, bare, and the two `Basic` forms) observed on the wire, an empty slot failing as `auth_required` rather than reaching the downstream, a value carrying a control character refused before framing and absent from every surface — `call_tool`, `status`, the Test result, the payload-free activity event, and the thrown error — rotation replacing the cached client and a connect already in flight while a wiped value fails the next call, the Test action's catalog probe and scope close, the cleartext-destination warning, and the vault and `authorize_connector` handoff end to end |
269
267
  | `remote-mcp-pagination.test.ts` | the `tools/list` cursor chain in both directions — exact cursor handoff, first-wins dedup, a failed later page rejecting rather than returning its prefix, the runaway backstops, the tool-metadata re-prime across pages, and paginated catalogs reaching the discovery path |
270
268
  | `request-admission.test.ts` | `/mcp` bounded before auth, the stable 503 and `Retry-After`, health and operator responsiveness under saturation, payload-free counters, queued cancellation, shutdown rejection while active work drains, and the separate fallback code pool |
271
269
  | `result-shapes.test.ts` | passive output-shape learning: value-free bounded inference, merging, 256-entry LRU eviction, 24-hour expiry, runtime isolation, read-only admission, declared-schema precedence, definition-change invalidation, discovery provenance, and failure isolation |
@@ -287,7 +285,7 @@ justification for *not* re-running it in workerd, so "it was easier" is not one.
287
285
 
288
286
  | Suite | Covers | Why Node |
289
287
  | --- | --- | --- |
290
- | `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 |
288
+ | `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, explicit optional modules 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 |
291
289
  | `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 |
292
290
  | `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 |
293
291
  | `drift-check.test.ts` | the credential-free maintainer drift checker: recorded touched endpoints, heading, table, and inline MCP inventories, setup-only providers, live-schema ownership, a quiet revision bump, clear failures for unavailable inputs, `$ref` traversal, and one well-formed row per endpoint | spawns the checker against filesystem fixtures |
@@ -310,7 +308,7 @@ justification for *not* re-running it in workerd, so "it was easier" is not one.
310
308
 
311
309
  | Suite | Covers |
312
310
  | --- | --- |
313
- | `browser/operator-ui.spec.ts` | the operator wiring in a real browser: Clerk loader order across its version redirect and a real load failure, the shell staying open until authentication, credential and access-token and OAuth flows end to end, drift shown without naming a tool, and every failure and empty state |
311
+ | `browser/operator-ui.spec.ts` | the operator wiring in a real browser: Clerk loader order across its version redirect and a real load failure, the shell staying open until authentication, credential and OAuth flows end to end, drift shown without naming a tool, and every failure and empty state |
314
312
 
315
313
  **The `_transportFactory` seam.** `RemoteMcpOptions._transportFactory` is
316
314
  internal, not public API: when set, `remoteMcp()` uses that `Transport` instead
@@ -4,109 +4,87 @@ The browser surface a human uses to see what a deployment exposes and to manage
4
4
  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
- Read [`ethos.md`](../ethos.md) first. The boundary this subsystem lives inside
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
- `test/operator-boundary.test.ts` proves it after every mutation route.
13
-
14
- Both deployment shapes ship the whole feature set behind it, because pages for
15
- things a deployment cannot do are worse than no pages
16
- ([#345](https://github.com/zackbart/connecta/issues/345)). The
17
- [Node template](../templates/node/) carries sign-in, vault, tokens, and
18
- activity as commented blocks in `src/index.ts` — plus a deployment-owned
19
- `src/file-activity.ts` that is compiled rather than commented — and the
20
- [Worker example](../examples/worker/) wires the first three and comments the
21
- fourth, which needs a D1 database. Each README walks through its own
22
- enablement.
23
-
24
- The vault is the one whose page needs a second thing. `/credentials` lists
25
- connector credential slots, so `credentialManagement` stays `no_slots` and
26
- the nav entry stays hidden until a connector declares `credential`, however
27
- well-configured the vault is. Neither shape's shipped connectors need a secret,
28
- so both carry the slot's shape as a comment beside a connector and say so in
29
- their walkthrough, which is the honest version of the same page count.
30
-
31
- ## The shape
32
-
33
- | Piece | What it owns |
34
- | --- | --- |
35
- | `src/ui.ts` | The served HTML: branding, gated URLs, CSP-nonced script tags, the four page titles, and `buildUiData` — the `/ui/data` payload. |
36
- | `src/operator-ui/model.ts` | The transport types both sides share, plus connector filtering. |
37
- | `src/operator-ui/view.ts` | The app's state shape and every pure rule over it. No DOM, so `test/ui.test.ts` calls it directly. |
38
- | `src/operator-ui/app/` | The browser app: `store.ts` (state and every request), `main.tsx` (shell, gate, router), and one component file per page. |
39
- | `src/operator-ui/browser.css` | One stylesheet, inlined into the shell. |
40
- | `src/operator-ui/generated.ts` | The build output: the bundle and the stylesheet as two exported strings. |
41
-
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
- 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
-
64
- This runtime selection is the Clerk migration seam. A deployment may contain
65
- both providers: before Worker-level Access is attached, the data-free shell
66
- selects Clerk; after Access supplies `ctx.access`, it selects ambient auth. That
67
- is not two same-hostname gates running in parallel. Access is upstream and a
68
- request it rejects never reaches Clerk. Keeping Clerk in the array preserves a
69
- code-level rollback after Access is detached.
70
-
71
- The Clerk loader is intentionally blocking. The inline operator bundle calls
72
- `boot()` as soon as the parser reaches the end of the body, so a deferred Clerk
73
- script would make an expected parse-time gap look like a permanent network
74
- failure. Blocking also preserves the existing failure path: after a real
75
- loader error, the parser continues and `boot()` renders the Clerk load message.
76
- Clerk's redirect from the major-version loader URL to its pinned asset keeps
77
- the same ordering.
78
-
79
- ## Rules that are not obvious
80
-
81
- - **No operator data in the shell.** Every page serves the same markup. Connector,
82
- credential, token, and activity data arrives only through the authenticated
83
- `/ui/*` APIs, and the shell is identical whether or not a caller is signed in.
84
- - **One store, one identity.** `store.ts` is the only file that touches `fetch`,
85
- `localStorage`, Clerk, or the ambient Access mode. Every token-bearing request carries the current session's token,
86
- while Access requests deliberately carry none,
87
- and every response is dropped unless the identity that asked for it is still
88
- the one on screen. `resetIdentity` replaces all identity-scoped state at once
89
- and bumps a generation that work already in flight compares itself against.
90
- - **Escaping is structural.** Components return elements; nothing builds HTML
91
- from strings. A value that could be a URL passes `safeHttpHref` before it may
92
- become an `href`, mirroring the server-side gate in `src/ui.ts`.
93
- - **Secrets are shown once.** A created access token lives in state only, and
94
- leaving the page — by navigation or by `pagehide`, which covers the
95
- back-forward cache — unmounts it.
96
- - **Every flow has four states.** Loading, error, empty, and success, with no
97
- dead end: a failed save keeps the form and its typed value, a failed list
98
- offers a retry, and an empty collection says what would fill it. A mutation
99
- that fails is still a resolved promise — `mutate` lands the failure in state
100
- rather than rejecting — so a caller that clears a form must clear it on a
101
- confirmed success, never on resolution. `createAccessToken` returns that
102
- answer as a boolean for exactly this reason.
103
- - **Drift is counts, and absence is its own answer.** The connector card reads
104
- `catalogDrift` ([#343](https://github.com/zackbart/connecta/issues/343)) as
105
- four category counts and a timestamp. There is no drill-down, because a tool
106
- name or a schema here would make an operator page the payload surface the
107
- drift model refuses to be. A connector with no report renders as *not
108
- observed*, never as clean: this runtime having seen no refresh is not the
109
- same claim as a refresh having found nothing.
7
+ Read [`ethos.md`](../ethos.md) first. Code declares capabilities and access;
8
+ the UI displays the current user's effective permissions and manages only
9
+ authentication material explicitly permitted by that code. It never edits the
10
+ connector set, tool catalog, annotations, OAuth scopes, or permission rules.
11
+
12
+ ## Enable the UI
13
+
14
+ ```ts
15
+ import { operatorUi } from "@zackbart/connecta/ui";
16
+
17
+ createConnecta({
18
+ connectors,
19
+ executor,
20
+ auth,
21
+ ui: operatorUi({ branding: { productName: "Team connections" } }),
22
+ });
23
+ ```
24
+
25
+ The UI module owns its browser bundle and routes. Omit `ui` to omit those
26
+ routes and runtime imports. OAuth callbacks remain in core; authorized
27
+ interactive MCP callers can complete consent without the UI. Branding belongs
28
+ to `operatorUi` options, with neutral callback branding when no UI is mounted.
29
+
30
+ ## Connections and activity
31
+
32
+ Connections is the main page. Each connection combines its status, effective
33
+ permissions, credential metadata, and permitted OAuth or credential actions.
34
+ There is no separate Credentials or Tokens tab. A user may see and invoke a
35
+ shared connector without permission to replace the grant everyone uses.
36
+ `identity.credentialAdministration` and `identity.personalConnection` select
37
+ shared and personal management rights, and both default to none.
38
+
39
+ Activity appears only when the optional history module has a readable store
40
+ and the caller passes `identity.activityAccess` and any additional read gate.
41
+ It is a global history, so permission to use one connector does not imply
42
+ permission to inspect that history. There is no member roster or policy editor.
43
+
44
+ The Node and Worker deployment READMEs show how to enable the modules and grant
45
+ the intended identities access. The configured bearer in the Node template can
46
+ read connection status but never mutate credentials as an interactive human.
47
+
48
+ ## Loading and request lifetime
49
+
50
+ The server shell contains no connector or credential data. Authenticated
51
+ `/ui/data` returns the configured visible connection list without waiting for
52
+ provider status or tool discovery. Details load through `GET /ui/connectors/<id>`, independently,
53
+ under a bounded request lifetime. Unknown and loading states stay explicit;
54
+ a provider failure leaves the other connections usable.
55
+
56
+ A status read does not start OAuth or create authorization handoffs. Connect is
57
+ an explicit authorized POST. Successful save, reconnect, and disconnect actions
58
+ show their result without waiting for an unrelated full-catalog reload. Server
59
+ mutations still await catalog invalidation before replying, so another request
60
+ cannot consume a persisted catalog from before a credential change.
61
+
62
+ Each details request owns and closes its downstream connector scope. Never
63
+ cache a transport, request signal, or awaited promise in the UI module.
64
+
65
+ ## Browser identity and security
66
+
67
+ Cloudflare Access is ambient browser auth. When the Worker invocation has
68
+ `ctx.access`, the shell emits no Clerk loader or browser-readable token.
69
+ Same-origin fetch carries the HttpOnly Access cookie, and the server uses the
70
+ trusted runtime identity. Sign out navigates to `/cdn-cgi/access/logout`.
71
+ Clerk deployments use their configured interactive provider.
72
+
73
+ Mutation requires exact same-origin `Origin`, an interactive identity,
74
+ connector visibility, and the relevant management permission. Personal actions
75
+ resolve only to the current principal's partition. Credential reads return
76
+ metadata, never saved values or masked fragments. Mutation cannot change any
77
+ declared capability. `test/operator-boundary.test.ts` checks that boundary.
78
+
79
+ The browser store fences responses by identity generation. Switching identity
80
+ clears the prior identity's state and discards its outstanding responses.
81
+ Components render elements, not HTML strings; links pass the shared URL gate.
82
+ Loading, failure, empty, and success states must all provide a useful next
83
+ step. A failed mutation preserves form input and does not masquerade as success.
84
+
85
+ Catalog drift remains counts and a timestamp. A missing observation means
86
+ "not observed", not that the downstream catalog is unchanged. The UI does not
87
+ expose tool schemas or raw payloads as diagnostics.
110
88
 
111
89
  ## Working on it
112
90
 
@@ -127,7 +105,7 @@ well as Node and there is no DOM in either:
127
105
  DOM-lib program (`tsconfig.operator-ui.json`) because it imports the store.
128
106
  - `test/browser/operator-ui.spec.ts` — the wiring, in a real browser:
129
107
  Clerk loader order across its version redirect and a real load failure, plus
130
- credential, token, and OAuth flows end to end, including their failure and
108
+ credential and OAuth flows end to end, including their failure and
131
109
  empty states. Run it with `npm run test:browser`
132
110
  (`npm run test:browser:install` once, for Chromium). It is not part of
133
111
  `npm run check`.