@zackbart/connecta 0.23.0 → 0.24.1

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 (81) hide show
  1. package/AGENTS.md +5 -0
  2. package/CHANGELOG.md +53 -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/execute.d.ts +6 -0
  20. package/dist/execute.js +17 -7
  21. package/dist/index.d.ts +35 -56
  22. package/dist/index.js +34 -58
  23. package/dist/invocation.js +19 -4
  24. package/dist/meta-tools.d.ts +4 -0
  25. package/dist/meta-tools.js +8 -4
  26. package/dist/module-contracts.d.ts +19 -0
  27. package/dist/module-contracts.js +1 -0
  28. package/dist/operator-ui/generated.js +2 -2
  29. package/dist/operator-ui/model.d.ts +6 -3
  30. package/dist/operator-ui/view.d.ts +2 -18
  31. package/dist/operator-ui/view.js +3 -20
  32. package/dist/registry.d.ts +4 -1
  33. package/dist/registry.js +4 -6
  34. package/dist/routes/activity.js +1 -1
  35. package/dist/routes/credentials.js +5 -2
  36. package/dist/routes/mcp.js +13 -3
  37. package/dist/routes/oauth-management.d.ts +2 -0
  38. package/dist/routes/oauth-management.js +108 -0
  39. package/dist/routes/oauth.d.ts +0 -1
  40. package/dist/routes/oauth.js +21 -121
  41. package/dist/routes/shared.d.ts +23 -17
  42. package/dist/routes/shared.js +48 -44
  43. package/dist/routes/ui.js +36 -33
  44. package/dist/server.js +6 -26
  45. package/dist/types.d.ts +2 -0
  46. package/dist/ui.d.ts +15 -70
  47. package/dist/ui.js +176 -317
  48. package/dist/version.d.ts +1 -1
  49. package/dist/version.js +1 -1
  50. package/documentation/architecture.md +26 -17
  51. package/documentation/auth.md +61 -106
  52. package/documentation/cloudflare.md +1 -1
  53. package/documentation/code-mode.md +2 -2
  54. package/documentation/connectors.md +1 -1
  55. package/documentation/linear.md +1 -1
  56. package/documentation/meta-tools.md +6 -4
  57. package/documentation/mixpanel.md +1 -1
  58. package/documentation/notion.md +2 -2
  59. package/documentation/operations.md +14 -14
  60. package/documentation/operator-ui.md +82 -104
  61. package/documentation/optional-modules-upgrade.md +243 -0
  62. package/documentation/provider-conventions.md +5 -3
  63. package/documentation/revenuecat.md +1 -1
  64. package/documentation/storage-and-credentials.md +59 -40
  65. package/documentation/stripe.md +1 -1
  66. package/documentation/upgrading.md +33 -4
  67. package/ethos.md +22 -30
  68. package/examples/worker/AGENTS.md +3 -1
  69. package/examples/worker/README.md +68 -84
  70. package/examples/worker/src/d1-activity.ts +1 -1
  71. package/examples/worker/src/index.ts +11 -6
  72. package/package.json +17 -1
  73. package/templates/node/AGENTS.md +8 -6
  74. package/templates/node/README.md +56 -67
  75. package/templates/node/package.json +1 -1
  76. package/templates/node/src/file-activity.ts +1 -1
  77. package/templates/node/src/index.ts +11 -12
  78. package/dist/access-tokens.d.ts +0 -31
  79. package/dist/access-tokens.js +0 -236
  80. package/dist/routes/access-tokens.d.ts +0 -6
  81. package/dist/routes/access-tokens.js +0 -83
@@ -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`.
@@ -0,0 +1,243 @@
1
+ # Upgrade to 0.24.0 optional modules
2
+
3
+ This is a breaking configuration change. Core still owns the same seven tools,
4
+ connector discovery, execution, invocation, and enforcement. UI, encrypted
5
+ credential storage, activity history, and configured bearer authentication now
6
+ have explicit imports. Select features when constructing the deployment;
7
+ there is no runtime plugin installation or registration.
8
+
9
+ Do this migration in the deployment repository on a branch. Do not re-run
10
+ `connecta init`, replace the connector set, or copy a template over local code.
11
+ The examples below omit unchanged connectors, executors, and storage adapters.
12
+ Install and pin `@zackbart/connecta@0.24.0` after completing the migration.
13
+
14
+ ### Configuration and import changes
15
+
16
+ | Before | After |
17
+ | --- | --- |
18
+ | UI implicitly available | Import `operatorUi` from `@zackbart/connecta/ui` and set `ui: operatorUi()` |
19
+ | Root `branding` | `ui: operatorUi({ branding })` |
20
+ | `credentials: { encryptionKey }` | Import `encryptedCredentialVault` from `@zackbart/connecta/credentials`; set `vault: encryptedCredentialVault(storage, encryptionKey)` |
21
+ | `activity: { store, deploymentId, readGate }` | Import `activityHistory` from `@zackbart/connecta/activity`; set `activity: activityHistory({ store, deploymentId, readGate })` |
22
+ | Root `bearerToken` import | Import from `@zackbart/connecta/auth/bearer`; preserve its token and subject configuration |
23
+ | `accessTokens: { ... }` | Remove after migrating every client that uses a Connecta-issued token |
24
+ | `identity.operatorAccess` | `identity.activityAccess` for global activity reads; grant auth management separately |
25
+ | Visibility implicitly permits auth changes | Explicit `credentialAdministration` for shared auth and `personalConnection` for personal auth; both default to `"none"` |
26
+
27
+ Removed configuration is refused at construction. Do not silence TypeScript
28
+ errors with casts; they identify decisions this upgrade needs you to make.
29
+
30
+ Before:
31
+
32
+ ```ts
33
+ import { createConnecta, bearerToken } from "@zackbart/connecta";
34
+
35
+ const connecta = createConnecta({
36
+ connectors,
37
+ executor,
38
+ storage,
39
+ auth: [bearerToken(token), interactiveAuth],
40
+ branding,
41
+ credentials: { encryptionKey },
42
+ accessTokens: {},
43
+ activity: { store: activityStore, deploymentId: "production" },
44
+ identity: {
45
+ connectorAccess,
46
+ operatorAccess: isOwner,
47
+ },
48
+ });
49
+ ```
50
+
51
+ After, with the same features except issued tokens:
52
+
53
+ ```ts
54
+ import { createConnecta } from "@zackbart/connecta";
55
+ import { bearerToken } from "@zackbart/connecta/auth/bearer";
56
+ import { operatorUi } from "@zackbart/connecta/ui";
57
+ import { encryptedCredentialVault } from "@zackbart/connecta/credentials";
58
+ import { activityHistory } from "@zackbart/connecta/activity";
59
+
60
+ const connecta = createConnecta({
61
+ connectors,
62
+ executor,
63
+ storage,
64
+ auth: [bearerToken(token), interactiveAuth],
65
+ ui: operatorUi({ branding }),
66
+ vault: encryptedCredentialVault(storage, encryptionKey),
67
+ activity: activityHistory({ store: activityStore, deploymentId: "production" }),
68
+ identity: {
69
+ connectorAccess,
70
+ credentialAdministration: ({ principal }) =>
71
+ principal && isOwner(principal) ? "all" : "none",
72
+ personalConnection: () => "all",
73
+ activityAccess: isOwner,
74
+ },
75
+ });
76
+ ```
77
+
78
+ `personalConnection: () => "all"` permits visible personal connections for
79
+ interactive principals. It does not permit shared grant changes or create a
80
+ principal for bearer or service identities. Use an explicit list of connector
81
+ ids if only some personal connections should be connectable.
82
+
83
+ ### Decide permissions explicitly
84
+
85
+ A teammate who can use a shared connector should not automatically be able to
86
+ replace the credentials everybody uses. Preserve the existing
87
+ `connectorAccess` resolver, then add the two management resolvers. They return
88
+ `"all"`, `"none"`, or declared connector ids and intersect with visibility.
89
+ Thrown resolvers and unknown ids fail closed. Literal deployment-provided
90
+ secrets remain configuration; UI permissions do not make them editable.
91
+
92
+ For a team Worker using Cloudflare Access:
93
+
94
+ ```ts
95
+ import { cloudflareAccessAuth } from "@zackbart/connecta/auth/cloudflare-access";
96
+
97
+ import type { ConnectaConfig } from "@zackbart/connecta";
98
+
99
+ const identity: NonNullable<ConnectaConfig["identity"]> = {
100
+ connectorAccess: ({ principal }) =>
101
+ principal?.id === "owner-access-id" ? "all" : ["shared_docs", "personal_linear"],
102
+ credentialAdministration: ({ principal }) =>
103
+ principal?.id === "owner-access-id" ? "all" : "none",
104
+ personalConnection: () => ["personal_linear"],
105
+ activityAccess: ({ id }) => id === "owner-access-id",
106
+ };
107
+
108
+ createConnecta({
109
+ connectors, executor, storage,
110
+ auth: cloudflareAccessAuth(),
111
+ identity,
112
+ ui: operatorUi(),
113
+ vault: encryptedCredentialVault(storage, encryptionKey),
114
+ });
115
+ ```
116
+
117
+ Use ids declared in this deployment and the real identity provider's stable
118
+ principal id. The strings above are illustrative, not a BePresent access
119
+ policy. Retain the Worker `ctx` forwarding and Worker-level Access application
120
+ with Managed OAuth. Access owns admission and identity; these resolvers own
121
+ Connecta permissions. Do not introduce a second user roster.
122
+
123
+ For a personal Node deployment with Clerk and a configured client bearer:
124
+
125
+ ```ts
126
+ import type { AuthenticatedIdentity } from "@zackbart/connecta";
127
+
128
+ const isOwner = ({ principal }: AuthenticatedIdentity) =>
129
+ principal?.id === "owner-clerk-id";
130
+
131
+ createConnecta({
132
+ connectors, executor, storage,
133
+ auth: [bearerToken(token, { subjectId: "static-token" }), clerk],
134
+ ui: operatorUi({ branding }),
135
+ vault: encryptedCredentialVault(storage, encryptionKey),
136
+ identity: {
137
+ connectorAccess: () => "all",
138
+ credentialAdministration: identity => isOwner(identity) ? "all" : "none",
139
+ personalConnection: identity => isOwner(identity) ? "all" : "none",
140
+ activityAccess: ({ id }) => id === "owner-clerk-id",
141
+ },
142
+ });
143
+ ```
144
+
145
+ This preserves shared-connector MCP access for the configured bearer while
146
+ reserving auth changes for the owner signing in through Clerk. The bearer does
147
+ not inherit the owner's personal connections. To make invocation owner-only
148
+ as well, restrict inbound admission or `connectorAccess` explicitly. Do not
149
+ replace an existing identity resolver with `"all"` just to match this example.
150
+
151
+ `activityAccess` receives an `IdentityReference` with `id` and `namespace`,
152
+ while management resolvers receive an `AuthenticatedIdentity` with `principal`.
153
+ `activityAccess` defaults to interactive humans. Set it deliberately for a team
154
+ if global activity must remain owner-only. Omitting it is broader than an old
155
+ owner-only `operatorAccess` resolver.
156
+
157
+ ### Migrate issued-token clients before upgrading
158
+
159
+ Connecta no longer issues or authenticates `cta_` tokens. The Tokens tab and
160
+ its creation, rename, and revocation endpoints are removed. Existing token
161
+ secrets immediately stop admitting requests to the upgraded deployment;
162
+ removing `accessTokens` alone does not migrate clients.
163
+
164
+ 1. Inventory clients using issued tokens while the old deployment can still
165
+ identify them. Do not paste secrets into logs, documentation, or a migration
166
+ issue.
167
+ 2. Move interactive clients to the configured identity provider's OAuth flow.
168
+ On Workers, use Cloudflare Access Managed OAuth and keep the required client
169
+ redirect allowlist. On Node, use the configured interactive provider.
170
+ 3. If an actual unattended Node client remains, configure the optional
171
+ `bearerToken` adapter and update that client. Preserve stable `subjectId`
172
+ values for existing configured bearers. On Workers, Access service tokens
173
+ provide edge admission; a Connecta bearer alone cannot cross Access.
174
+ 4. Verify each replacement client before removing `accessTokens` and deploying
175
+ the new package. An issued token previously inherited its creator's personal
176
+ principal; a configured bearer does not. Clients needing personal grants
177
+ must use a human identity flow.
178
+
179
+ Stored issued-token records become inert. They are not deleted automatically
180
+ and are not converted into configured bearer credentials. No storage sweep is
181
+ required for this upgrade. If rolling back to an older binary, remember that it
182
+ understands those old records again; retire or revoke old tokens in the old
183
+ system before migration if rollback must not restore them.
184
+
185
+ ### Preserve vault and OAuth state
186
+
187
+ Pass the existing storage adapter and the same encryption key to
188
+ `encryptedCredentialVault`. Do not generate a replacement key as part of this
189
+ refactor. Keep connector ids, identity namespaces, principal ids, and owner
190
+ partitions stable. The encrypted vault binds connector and owner into its
191
+ AES-GCM context; moving records between owners does not migrate ownership.
192
+
193
+ No vault record, OAuth grant, or catalog storage-format migration is required.
194
+ Downstream OAuth continues to use connector storage, independently of the vault
195
+ module. Existing activity stores and retention policies remain deployment-owned;
196
+ wrap the existing store in `activityHistory` rather than creating a new database.
197
+ A replacement `CredentialVault` must preserve connector and owner isolation.
198
+
199
+ ### Omit features you do not need
200
+
201
+ Omit `ui` and its import for an API-only deployment. UI shells, data APIs, and
202
+ auth mutation routes disappear. Core `/mcp`, `/health`, auth metadata, and OAuth
203
+ callbacks remain. An authorized interactive MCP caller can use
204
+ `authorize_connector` and complete OAuth without UI. Static credential recovery
205
+ returns `unavailable` without a mounted UI or vault, instead of a missing page.
206
+ UI-free callbacks use neutral branding and offer no dead return link.
207
+
208
+ Omit `activity` and its imports to stop recording and remove the Activity tab.
209
+ Remove deployment-only store wiring or database bindings only if nothing else
210
+ uses them. Existing history is not deleted. Diagnostic output is separate;
211
+ set `logger: "silent"` only when you also want to suppress it.
212
+
213
+ Omit `vault` when credentials come entirely from deployment configuration or
214
+ OAuth. A connector declaring a vault slot then remains unmanageable; do not
215
+ omit it while expecting stored vault credentials to keep resolving.
216
+
217
+ There is no generic module list. Keep ordinary explicit imports and the typed
218
+ configuration slots. The implementations may ship in the package, but core
219
+ does not load them merely because the package is installed.
220
+
221
+ ### Verify the upgraded deployment
222
+
223
+ - Run the deployment's typecheck and build with the new imports and exact pin.
224
+ Keep its connector declarations, executor, and custom routing intact.
225
+ - Run `connecta doctor` against the running deployment using its actual inbound
226
+ authentication. Confirm the same seven tools and a working executor.
227
+ - Sign in as an owner and a teammate. Confirm visibility, shared auth controls,
228
+ personal connection controls, and activity access match code. A member may
229
+ invoke a shared connector while having no authority to change its grant.
230
+ - Open Connections. Its configured list should appear before downstream details;
231
+ one slow or failing provider should not delay the other cards. OAuth starts
232
+ only after an explicit action, and action feedback does not await other probes.
233
+ - Read existing vault-backed and personal OAuth connections without re-entering
234
+ secrets. Exercise one permitted auth flow and confirm other principals cannot
235
+ manage it. With UI omitted, verify OAuth callback completion and the honest
236
+ unavailable response for static credential handoff.
237
+ - Update bookmarks and operator instructions to the connection-centered UI.
238
+ There is no separate Credentials or Tokens tab. Verify optional Activity and
239
+ the absence of UI routes in a deployment that omits the UI.
240
+
241
+ Keep the old pin, configuration, and storage backup available until these checks
242
+ pass. This guide changes package configuration; it does not authorize or perform
243
+ any deployment, credential rotation, or storage deletion.
@@ -416,13 +416,15 @@ default. The provider's own headless credential — a personal API key, a
416
416
  restricted key, a service account — is supported two ways: explicit `headers`
417
417
  auth, documented as a secret rather than configuration, and `{ type:
418
418
  "credential" }`, which declares an operator slot and takes the same secret from
419
- `/credentials` instead. Either way it is paired with the narrowest mode the
419
+ the connection UI at `/` instead. Either way it is paired with the narrowest mode the
420
420
  deployment can use, and the framing matches the provider's *published* contract
421
421
  for the MCP endpoint — not a convention borrowed from that provider's other
422
422
  APIs, and not this repository's earlier example, which is the same claim wearing
423
423
  a circle. `requireHttps` is set. Recovery from an expired authorization is the
424
424
  ordinary `auth_required` → `authorize_connector` route, which returns the
425
- consent URL for OAuth and the `/credentials` handoff for a declared slot.
425
+ consent URL for permitted OAuth starts and a connection UI handoff at `/` for
426
+ a declared slot when UI and vault are mounted. Missing modules yield
427
+ `unavailable`; never advertise a route the deployment cannot serve.
426
428
 
427
429
  *Why:* one route back from an expired credential is what keeps a failed call
428
430
  from becoming an abandoned task. *Cost:* wrong-tool selection.
@@ -442,7 +444,7 @@ deployment file to read — and a dead, revoked, or absent credential fails loud
442
444
  at use as `auth_required` with the `authorize_connector` route attached (P9).
443
445
 
444
446
  `testCredential` exists only behind the operator-pressed Test action on
445
- `/credentials`, and only for a declared slot. It connects with the stored value
447
+ the connection UI at `/`, and only for a declared slot. It connects with the stored value
446
448
  and reports how many tools the downstream served, which is the whole honest
447
449
  check for a proxy: which account, project, or mode a key reaches is the
448
450
  provider's answer, not Connecta's. That is not the shape
@@ -81,7 +81,7 @@ connectors: [
81
81
  ```
82
82
 
83
83
  Neither key has to be a runtime secret. Declare the slot instead and each
84
- connector's key is pasted, tested, and rotated on `/credentials`:
84
+ connector's key is pasted, tested, and rotated on the connection UI at `/`:
85
85
 
86
86
  ```ts
87
87
  connectors: [
@@ -1,39 +1,59 @@
1
1
  # Storage and credentials
2
2
 
3
- The core `KVStorage` seam supports `get`, `set`, and `delete`; adapters may also
4
- implement `list(prefix)`. Named access tokens require listing because every
5
- token is an independent record rather than one shared, race-prone manifest.
6
- The built-in memory and file adapters implement it, as does the Cloudflare KV
7
- example.
8
-
9
- Connectors may declare a human-managed `credential` slot. When
10
- `credentials.encryptionKey` is configured, Connecta encrypts values in the
11
- deployment storage and exposes read-only access only through that connector's
12
- `ctx.credential`. Values, masked values, call arguments, and raw errors never
13
- enter model-facing recovery responses or activity records.
14
-
15
- Proactive credential liveness probing was **removed in 0.9** by ethos decision
16
- ([#179](https://github.com/zackbart/connecta/issues/179)). The vault, local
17
- credential-shape drift detection, and operator-triggered credential tests remain.
18
-
19
- Credentials fail at use. A typed `auth_required` response directs the agent to
20
- `authorize_connector`, which returns one of the recovery modes documented in
21
- [meta-tools](./meta-tools.md#authorization-recovery). A declared slot with a
22
- configured vault returns a secret-free `/credentials` handoff. A missing vault
23
- returns `recovery: "unavailable"` and names `credentials.encryptionKey`;
24
- Connecta also warns at startup.
25
-
26
- Credential mutation is intentionally narrower than MCP access:
27
-
28
- - a static bearer may call tools and receive the operator handoff, but it
29
- cannot write credentials;
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
33
- - saving, replacing, testing, or removing a value never returns that value.
34
-
35
- The vault is read for each call. Once a signed-in human saves a replacement,
36
- the agent can retry immediately without restarting or redeploying Connecta.
3
+ The core `KVStorage` contract supports `get`, `set`, and `delete`; adapters may
4
+ also implement `list(prefix)`. Core uses storage for connector state, catalogs,
5
+ and result paging. The vault is an explicit independent option:
6
+
7
+ ```ts
8
+ import { encryptedCredentialVault } from "@zackbart/connecta/credentials";
9
+
10
+ createConnecta({
11
+ connectors,
12
+ executor,
13
+ storage,
14
+ vault: encryptedCredentialVault(storage, encryptionKey),
15
+ });
16
+ ```
17
+
18
+ `encryptionKey` is a base64 32-byte AES key. The factory implements the
19
+ root-exported `CredentialVault` contract. Core depends on the contract without
20
+ importing the encrypted implementation; deployments may supply their own.
21
+ A replacement must isolate both connector id and optional owner, preserve
22
+ metadata-only reads for the UI, and keep plaintext confined to host credential
23
+ resolution. The built-in AES-GCM implementation binds connector and owner into
24
+ authenticated encryption context. Sharing a backend is not permission to share
25
+ a principal's credentials.
26
+
27
+ Connectors declare a human-managed `credential` slot in code. The vault exposes
28
+ read access only through that connector's `ctx.credential`. Values, masked
29
+ values, call arguments, and raw errors never enter model recovery or activity.
30
+ Omitting the vault leaves literal deployment-provided secrets and downstream
31
+ OAuth available, but declared vault slots cannot be managed.
32
+
33
+ Credentials fail at use. Proactive liveness probing remains removed by
34
+ [the 0.9 decision](../ethos.md). Operator-triggered tests and local credential
35
+ shape checks remain. `authorize_connector` returns a human credential handoff
36
+ only when both a vault and the UI are configured. Without either, recovery is
37
+ `unavailable` and explains the missing configuration.
38
+
39
+ A static bearer may invoke a connector but never mutate its auth as a browser
40
+ user. Interactive users need visibility plus `credentialAdministration` for
41
+ shared auth or `personalConnection` for their own personal auth. Both management
42
+ permissions default to none. Saving, testing, replacing, or removing a value
43
+ never returns it. The vault is read for each call, so a saved replacement takes
44
+ effect without restarting the deployment.
45
+
46
+ ## Storage continuity
47
+
48
+ This module extraction changes no encrypted record keys, owner partitions, or
49
+ OAuth storage format. Reuse the existing storage and encryption key when
50
+ constructing `encryptedCredentialVault`. Keep connector ids, principal ids, and
51
+ identity namespaces unchanged. Losing the encryption key makes old vault
52
+ values unreadable; changing identity context selects another personal partition.
53
+
54
+ Removed Connecta-issued token records remain inert in storage. This upgrade
55
+ does not delete them, convert them to another credential, or migrate their
56
+ clients automatically.
37
57
 
38
58
  ## Shared and personal auth
39
59
 
@@ -54,8 +74,7 @@ Personal connectors disappear from a request that has no stable human
54
74
  principal. For a principal that can see one, connecta partitions connector
55
75
  storage, encrypted vault records, catalog caches, OAuth generations, and
56
76
  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.
77
+ `get_result` are partitioned by the authenticated subject, so one authenticated subject cannot page another subject's call.
59
78
 
60
79
  Literal `auth: { type: "headers" }` cannot be personal because its secret lives
61
80
  in deployment code. `remoteMcp()` refuses that combination at construction.
@@ -73,7 +92,7 @@ remoteMcp("revenuecat_bepresent", {
73
92
  ```
74
93
 
75
94
  The connector, its endpoint, and the credential *slot* stay declared in code;
76
- only the secret arrives through `/credentials`. That is the same boundary
95
+ only the secret arrives through the connection UI at `/`. That is the same boundary
77
96
  `api()` has always had, and the reason a project-wide key no longer has to be a
78
97
  Worker secret or an environment variable
79
98
  ([#439](https://github.com/zackbart/connecta/issues/439)).
@@ -99,8 +118,8 @@ redaction that keeps part of a secret is still a leak.
99
118
  An empty slot is not a boot failure and not a silently absent connector. The
100
119
  connector is present, its status reads `auth_required`, calls fail with the same
101
120
  typed error a missing OAuth grant produces, and `authorize_connector` returns
102
- the `/credentials` handoff. With no vault configured at all, the failure names
103
- `credentials.encryptionKey`, and Connecta already warned at startup.
121
+ a connection UI handoff at `/` when UI and vault are configured. Without either,
122
+ it returns `unavailable` and explains what is missing.
104
123
 
105
124
  The vault is read before any cached downstream client is trusted, so a rotation
106
125
  lands on the next call rather than the next deploy. Connecta compares a SHA-256
@@ -111,7 +130,7 @@ message. A cleartext `http://` destination warns at construction here exactly as
111
130
  it does for literal headers — who owns the secret changed, not what the wire
112
131
  carries.
113
132
 
114
- `/credentials`' Test action connects with the stored value and reports how many
133
+ The connection UI Test action connects with the stored value and reports how many
115
134
  tools the downstream served. That is the whole honest check for a proxy: which
116
135
  account, project, or mode the key reaches is the provider's answer, not
117
136
  Connecta's.
@@ -95,7 +95,7 @@ Use a restricted key, not a secret key, and scope it to the operations the
95
95
  agent actually needs; Stripe's own guidance is to "limit your agent's access to
96
96
  exactly the functionality it requires". Keep it in the runtime's secret store.
97
97
 
98
- The same key can come from `/credentials` instead:
98
+ The same key can come from the connection UI at `/` instead:
99
99
 
100
100
  ```ts
101
101
  stripe("stripe_sandbox", {