@zackbart/connecta 0.31.0 → 0.33.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 (169) hide show
  1. package/CHANGELOG.md +97 -0
  2. package/README.md +30 -20
  3. package/bin/connecta.mjs +25 -6
  4. package/bin/version.mjs +1 -1
  5. package/dist/access-tokens.d.ts +167 -23
  6. package/dist/access-tokens.js +676 -140
  7. package/dist/agent-identity.js +47 -0
  8. package/dist/auth/cloudflare-access.js +13 -5
  9. package/dist/auth-elicitation.js +1 -1
  10. package/dist/call-admission-context.js +15 -0
  11. package/dist/call-admission.js +30 -43
  12. package/dist/call-recovery.js +6 -2
  13. package/dist/catalog-service.js +7 -2
  14. package/dist/config-value-sources.js +14 -9
  15. package/dist/config.d.ts +58 -78
  16. package/dist/config.js +143 -47
  17. package/dist/connector-access.d.ts +4 -33
  18. package/dist/connector-access.js +15 -99
  19. package/dist/connector-caller.d.ts +7 -1
  20. package/dist/connector-caller.js +3 -2
  21. package/dist/connectors/api-connector.d.ts +27 -4
  22. package/dist/connectors/api-connector.js +2 -0
  23. package/dist/connectors/guarded-fetch.d.ts +9 -0
  24. package/dist/connectors/guarded-fetch.js +7 -0
  25. package/dist/connectors/remote-mcp.js +2 -0
  26. package/dist/describe-config.d.ts +32 -7
  27. package/dist/describe-config.js +9 -18
  28. package/dist/errors.d.ts +1 -1
  29. package/dist/errors.js +1 -0
  30. package/dist/execute.js +16 -14
  31. package/dist/index.d.ts +4 -3
  32. package/dist/index.js +30 -10
  33. package/dist/invocation.js +12 -7
  34. package/dist/membership.d.ts +51 -0
  35. package/dist/membership.js +121 -0
  36. package/dist/meta-tools.js +17 -19
  37. package/dist/module-contracts.d.ts +23 -6
  38. package/dist/operator-record.d.ts +2 -2
  39. package/dist/operator-record.js +2 -2
  40. package/dist/operator-ui/agents.d.ts +74 -0
  41. package/dist/operator-ui/agents.js +104 -0
  42. package/dist/operator-ui/contract.d.ts +151 -29
  43. package/dist/operator-ui/endpoints.js +192 -0
  44. package/dist/operator-ui/fix-prompts.js +4 -4
  45. package/dist/operator-ui/generated.js +5 -4
  46. package/dist/operator-ui/member-page.js +112 -0
  47. package/dist/operator-ui/model.d.ts +15 -7
  48. package/dist/operator-ui/view.d.ts +84 -19
  49. package/dist/operator-ui/view.js +83 -21
  50. package/dist/page-styles.js +1 -1
  51. package/dist/program-writes.js +2 -2
  52. package/dist/provider.d.ts +63 -11
  53. package/dist/provider.js +46 -7
  54. package/dist/providers/_shared/google/workspace.d.ts +53 -16
  55. package/dist/providers/_shared/google/workspace.js +31 -9
  56. package/dist/providers/_shared/rest/tools.js +2 -2
  57. package/dist/providers/_shared/rest/value-safety.d.ts +50 -0
  58. package/dist/providers/_shared/rest/value-safety.js +699 -7
  59. package/dist/providers/breeze/skill.generated.js +2 -2
  60. package/dist/providers/ccb/index.js +1 -1
  61. package/dist/providers/cloudflare/index.js +3 -3
  62. package/dist/providers/cloudflare/rest.js +25 -3
  63. package/dist/providers/cloudflare/skill.generated.js +2 -2
  64. package/dist/providers/cloudflare/value-safety.js +32 -0
  65. package/dist/providers/docs/index.d.ts +1 -1
  66. package/dist/providers/docs/index.js +5 -5
  67. package/dist/providers/docs/skill.generated.js +2 -2
  68. package/dist/providers/drive/index.d.ts +1 -1
  69. package/dist/providers/drive/index.js +4 -4
  70. package/dist/providers/drive/skill.generated.js +2 -2
  71. package/dist/providers/forms/index.d.ts +1 -1
  72. package/dist/providers/forms/index.js +5 -5
  73. package/dist/providers/forms/skill.generated.js +2 -2
  74. package/dist/providers/github/skill.generated.js +2 -2
  75. package/dist/providers/gmail/index.d.ts +1 -1
  76. package/dist/providers/gmail/index.js +279 -26
  77. package/dist/providers/gmail/skill.generated.js +2 -2
  78. package/dist/providers/linear/index.js +1 -1
  79. package/dist/providers/mixpanel/index.d.ts +27 -12
  80. package/dist/providers/mixpanel/index.js +58 -6
  81. package/dist/providers/mixpanel/operations.js +20 -0
  82. package/dist/providers/mixpanel/rest.js +603 -0
  83. package/dist/providers/mixpanel/skill.generated.js +3 -2
  84. package/dist/providers/mixpanel/value-safety.js +16 -0
  85. package/dist/providers/notion/index.d.ts +7 -0
  86. package/dist/providers/notion/index.js +116 -11
  87. package/dist/providers/notion/rest.js +183 -24
  88. package/dist/providers/notion/skill.generated.js +4 -4
  89. package/dist/providers/notion/value-safety.js +30 -5
  90. package/dist/providers/overflow/index.js +1 -1
  91. package/dist/providers/planning-center/index.d.ts +27 -0
  92. package/dist/providers/planning-center/index.js +287 -25
  93. package/dist/providers/planning-center/skill.generated.js +24 -4
  94. package/dist/providers/sheets/index.d.ts +4 -1
  95. package/dist/providers/sheets/index.js +9 -6
  96. package/dist/providers/sheets/skill.generated.js +2 -2
  97. package/dist/providers/slides/index.d.ts +1 -1
  98. package/dist/providers/slides/index.js +6 -6
  99. package/dist/providers/slides/skill.generated.js +2 -2
  100. package/dist/providers/stripe/rest.js +9 -2
  101. package/dist/providers/stripe/skill.generated.js +2 -2
  102. package/dist/providers/stripe/value-safety.js +22 -0
  103. package/dist/providers/vercel/index.js +2 -2
  104. package/dist/providers/vercel/rest.js +20 -2
  105. package/dist/providers/vercel/skill.generated.js +2 -2
  106. package/dist/providers/vercel/value-safety.js +22 -0
  107. package/dist/providers/wispr-flow/index.d.ts +14 -0
  108. package/dist/providers/wispr-flow/index.js +117 -0
  109. package/dist/providers/wispr-flow/skill.generated.js +8 -0
  110. package/dist/registry.d.ts +2 -3
  111. package/dist/registry.js +8 -9
  112. package/dist/routes/access-tokens.js +82 -75
  113. package/dist/routes/activity-disclosure.js +17 -18
  114. package/dist/routes/activity.js +34 -54
  115. package/dist/routes/actor-labels.js +33 -4
  116. package/dist/routes/agent-path.js +43 -0
  117. package/dist/routes/connect.js +16 -12
  118. package/dist/routes/credentials.js +16 -40
  119. package/dist/routes/mcp.js +155 -85
  120. package/dist/routes/oauth-management.js +11 -5
  121. package/dist/routes/oauth.js +3 -9
  122. package/dist/routes/operator.js +25 -17
  123. package/dist/routes/shared.d.ts +60 -20
  124. package/dist/routes/shared.js +187 -96
  125. package/dist/routes/ui-config.js +437 -97
  126. package/dist/routes/ui-data.js +2 -2
  127. package/dist/routes/ui.js +9 -16
  128. package/dist/runtime/call-admission.js +9 -0
  129. package/dist/server.js +8 -0
  130. package/dist/skills.js +5 -4
  131. package/dist/storage/keys.js +9 -1
  132. package/dist/tool-safety.js +13 -5
  133. package/dist/types.d.ts +12 -1
  134. package/dist/ui.d.ts +2 -2
  135. package/dist/ui.js +17 -15
  136. package/dist/usage-guide.js +3 -3
  137. package/dist/version.d.ts +1 -1
  138. package/dist/version.js +1 -1
  139. package/dist/workspace-address.js +9 -0
  140. package/dist/workspace-subject.js +23 -0
  141. package/documentation/README.md +46 -22
  142. package/documentation/architecture.md +378 -58
  143. package/documentation/auth.md +806 -215
  144. package/documentation/code-mode.md +93 -81
  145. package/documentation/deploying.md +128 -26
  146. package/documentation/integrating.md +145 -22
  147. package/documentation/meta-tools.md +58 -46
  148. package/documentation/migration-0.33.md +344 -0
  149. package/documentation/operating.md +14 -9
  150. package/documentation/operator-tests.md +117 -31
  151. package/documentation/operator-ui.md +326 -55
  152. package/documentation/provider-auth-migration.md +42 -27
  153. package/documentation/provider-migration-0.29.md +3 -3
  154. package/examples/worker/AGENTS.md +35 -8
  155. package/examples/worker/README.md +92 -53
  156. package/examples/worker/src/connecta.config.ts +41 -20
  157. package/examples/worker/wrangler.jsonc +3 -0
  158. package/package.json +7 -2
  159. package/templates/node/.env.example +3 -0
  160. package/templates/node/AGENTS.md +33 -10
  161. package/templates/node/Dockerfile +2 -2
  162. package/templates/node/README.md +71 -44
  163. package/templates/node/docker-compose.yml +3 -2
  164. package/templates/node/package.json +4 -3
  165. package/templates/node/src/adopt-token.ts +23 -0
  166. package/templates/node/src/connecta.config.ts +33 -12
  167. package/templates/node/src/rotate-token.ts +17 -0
  168. package/dist/tool-safety.d.ts +0 -9
  169. package/templates/node/src/provision-token.ts +0 -17
@@ -10,6 +10,7 @@ credential ownership, and grants use [Auth](./auth.md).
10
10
  | Task | Read |
11
11
  | ----------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- |
12
12
  | Enable UI, vault, or history in a deployment | [Setup](#setup), then the platform template |
13
+ | Find a member's endpoint or set up their client | [Use the member page](#use-the-member-page) |
13
14
  | Consume config, provenance, status, or activity | [Operator data contract](#operator-data-contract) |
14
15
  | Change a UI page or data route | [Source and tests](#source-and-tests), [browser verification](./operator-tests.md) |
15
16
  | Diagnose permission refusals | [Management permissions](./auth.md#management-permissions), [activity disclosure below](#operator-data-contract) |
@@ -22,75 +23,326 @@ and [Worker example](../examples/worker/README.md#select-optional-modules)
22
23
  own the runnable configuration, variables, and branding shape. The UI is an
23
24
  explicit optional import and stays outside the root package import graph.
24
25
 
25
- Overview, Connectors, Tools, Access, Activity, and Config show only what the
26
- caller may inspect. Connector detail tabs cover configuration, schemas,
27
- authentication, activity, and diagnostics. Config labels values as default or
28
- configured. Deployment code owns connectors, grants, pools, and classification;
26
+ Endpoints, Connectors, Activity, Tokens, and Settings are the maintainer's
27
+ pages. Endpoints is the default page at `/`: one row for `/mcp` and each pool,
28
+ with a copyable URL, who can use it, tool count, endpoint mode, and client
29
+ setup. Who can use it is `root.maintainers` on `/mcp` and the pool's `members`
30
+ elsewhere: a person by label, or by raw id when the label is null, beside their
31
+ initials, and an agent as `agent:<name>` with a token mark. A row shows four
32
+ and a "+N more" button that reveals the rest. A pool with
33
+ agent members also shows its agent URL, muted. Expanding a row lists each
34
+ connector on that endpoint with what it runs as and its tools, marking write
35
+ tools. The runs-as label comes from that endpoint's matrix cell. A shared connector
36
+ names its credential's source: "Shared · saved <date>" (vault), "Shared ·
37
+ connected <date>" (OAuth), "Shared · not configured", "Shared · set in
38
+ deployment", or "Shared · no credential needed"; a vault or OAuth cell without a
39
+ date omits it. The others read "Personal · N of M connected", "Workspace · people:
40
+ self · agents: <owner>" (or "none"), or "Everyone acts as <subject>" for a
41
+ fixed-subject Workspace connector. Connector health is not repeated per
42
+ endpoint: a connector that needs attention gets a status dot linking to its
43
+ Connection tab. Counts intersect live tools with the pool's declared tools
44
+ (the `/mcp` matrix cells for the root), guarded grants' live verdicts, and
45
+ read-only mode. Connectors lists each connector with one status badge, auth
46
+ scope, tool count, and last call; a problem adds one action (Connect, Replace
47
+ key, or Inspect) that opens Connection. A List / Matrix toggle (`?view=matrix`)
48
+ switches to a table of connectors by endpoint, `/mcp` first: each cell reads
49
+ "all", a tool count, or a dash, read-only columns are tinted, and a Runs as
50
+ column summarizes the `/mcp` cell, else the first pool's. A cell with tools is
51
+ a button named like "GitHub on /mcp/support: 1 tool, read-only"; Enter, Space or
52
+ a click opens a popover listing exactly that cell's tool names, write tools
53
+ marked, with the cell's runs-as. Escape or Tab returns focus to the cell. The
54
+ popover opens below its cell, or above when the viewport has more room there,
55
+ with a capped height that scrolls; CSS anchor positioning places it, and the
56
+ page sets no inline styles. A browser without anchor positioning gets the same
57
+ list, with the same keys, in an expansion row under the connector. `GET /ui/api/config` is the browser's one gate: a 401
58
+ shows sign-in, the private 404 shows a "No access" page naming who to ask, with
59
+ Sign out or Change token, and a 200 renders by `role`. `/ui/data`, connector
60
+ details, activity and tokens are requested only after the contract says
61
+ `maintainer`, and every such read checks the role again when it is sent, focus
62
+ returns included. Until a role is confirmed, the page shows its loading or
63
+ error state and no navigation. The store keeps the last confirmed role through
64
+ a failed refresh: a maintainer's page stays mounted, inert and dimmed, under
65
+ one shell banner with Retry, whichever page it is, and sends nothing
66
+ maintainer-only until a contract confirms the role again. Recovery closes the
67
+ page's dialogs, drawers and popovers and the command palette, and moves focus
68
+ to Retry. Credential and OAuth writes also wait for a contract that agrees
69
+ with the confirmed role: a maintainer's for a connector it shows, a member's
70
+ for a personal connector their contract lists with that action in `manage`;
71
+ otherwise the write is not sent and its row says so. An
72
+ answer with a different role clears everything read under the old one and
73
+ admits the viewer again. A
74
+ pool member sees the [member page](#use-the-member-page) on every path, with
75
+ no tabs. Connector
76
+ detail has Tools, Connection, and Activity tabs. Connection combines
77
+ authentication (including the OAuth client mechanism), catalog diagnostics,
78
+ one fix prompt beside each problem it reports, and the connector snapshot with its source. Settings combines inbound-auth facts with the
79
+ configuration snapshot and labels values as default or configured. Deployment code owns connectors, grants, pools, and classification;
29
80
  the UI changes only authorized authentication material.
30
81
 
82
+ Tokens has one row per agent declared in code, in `GET /ui/access-tokens`
83
+ order: its pools, a state badge (No token, Issued or Revoked), the issue or
84
+ revoke date, and last seen ("never" when null). One line says agents are
85
+ declared in code; nothing creates or renames one. Issue appears for a slot with
86
+ no token or a revoked one, Rotate and Revoke for an issued one; rotate and
87
+ revoke ask first in a row under the agent's, with focus on Cancel. Every
88
+ mutation sends `{ "expect": "<revision>" }` with the slot revision the
89
+ operator confirmed against. A new secret opens once in a dialog with a copy
90
+ button and a note that it won't be shown again; closing it drops the secret
91
+ from the page, and recovery never closes it. A 409 `slot_changed` reads
92
+ "Changed since you looked, refresh"; a 503 `outcome_unknown` or 499 `aborted`
93
+ is a failure notice and is never retried. Sending a mutation leaves the slot
94
+ unresolved until its successful response is validated, which resolves it. Any
95
+ other outcome, a contract whose `agents[].token` disagrees with the list
96
+ (revision included) or drops the agent, or a failed read leaves it unresolved
97
+ until a slot-list read that started afterwards succeeds. Until then its
98
+ controls are off with "Refresh to continue", and each action is
99
+ checked again immediately before it is sent. Under the recovery banner Tokens
100
+ offers no Refresh of its own, and an open rotate or revoke question closes.
101
+
31
102
  Install the vault for declared credential slots and activity history for
32
103
  persisted call records. Neither module grants permission to manage or read
33
- it. Credential administration, personal connection, access-token management,
34
- and activity access are separate identity decisions, denied by default where
35
- specified in [auth](./auth.md#management-permissions). Machine tokens never
36
- grant interactive credential or token management authority.
104
+ it: [auth](./auth.md#management-permissions) does. Interactive maintainers
105
+ administer shared credentials and agent tokens, maintainers read activity, and
106
+ pool members connect their own accounts on personal connectors in their pools.
107
+ Token management issues, rotates, and revokes the tokens of agents declared in
108
+ code; it never creates, renames, or binds an agent. Agent tokens never grant
109
+ interactive credential or token management authority.
110
+
111
+ ## Use the member page
112
+
113
+ With `operatorUi()` enabled, sign in at the deployment's public URL using
114
+ Clerk (Node) or Cloudflare Access (Workers). A person listed in a pool's
115
+ `members` can open the page without becoming a maintainer. The page, **Your
116
+ connections**, has no tabs; its top bar shows branding, the signed-in account
117
+ (Clerk's primary email, when there is one) and Sign out or Change token. It
118
+ lists:
119
+
120
+ - **Your endpoints**: each admitted `/mcp/<pool>` URL with a copy button and
121
+ **Client setup**. For example, Bob in pool `team` copies
122
+ `https://connecta.example.com/mcp/team` into an MCP client and authenticates
123
+ as Bob.
124
+ - **Your accounts**: the personal connectors in their pools, each with one
125
+ status badge and only the controls its `manage.actions` lists. An OAuth
126
+ connector offers Connect, Reconnect or Disconnect; Reconnect and Disconnect
127
+ ask first. A credential connector offers **Add credential** when only `save`
128
+ is listed, and Replace, Test and Clear once a value is stored.
129
+ - **Shared with you**: shared connectors by name and tool count, without status.
130
+
131
+ Every browser path shows that same member page; it does not expose maintainer
132
+ connector-detail, activity, token, or settings pages, and it never sends a
133
+ maintainer request. After an action, and when the tab regains focus while an
134
+ authorization it started is still outstanding (up to ten minutes), it re-reads
135
+ `GET /ui/api/config`. Each action is checked against the current `manage`
136
+ again immediately before it is sent, and a refresh that drops an action closes
137
+ its open confirmation or form. If a refresh fails or misses its deadline, the
138
+ page shows its own error with Retry and moves focus to its heading; members
139
+ never see the maintainer recovery banner.
140
+
141
+ The page reads `GET /ui/api/config`: 200 with `role: "member"` confirms
142
+ membership; 401 means sign in; a private 404 means the signed-in person has
143
+ no maintainer role or human pool membership. A `cta_` agent token gets 401
144
+ and cannot sign in to this page. Use the agent's `/agent/<pool>` endpoint
145
+ for machine access instead.
146
+
147
+ Personal OAuth can also start from the person's `/mcp/<pool>` connection with
148
+ `authorize_connector`, finishing its signed browser link. Trusted clients that manage personal credentials use only the actions listed
149
+ in the connector's `manage` field; the API contract below describes their
150
+ routes and preconditions. Shared credentials always require a maintainer.
37
151
 
38
152
  ## Operator data contract
39
153
 
40
154
  `GET /ui/api/config`, owned by `operatorUi()`, returns `OperatorUiContract`
41
- from `connecta/ui`. `src/operator-ui/contract.ts` is also the browser's type
42
- source. The response has `schemaVersion: 1`, `config`, `configSources`, `live`, and `you`.
43
- `config` is the construction-time `describeConfig()` snapshot, filtered to
44
- visible connectors, granted tools, classification overrides, and admitted
45
- pools. Pool tools are the intersection with the caller's identity grants.
46
- `configSources` labels each serialized leaf as `default` or `config`, based on
47
- option presence rather than equality with the default. Factory descriptions carry
48
- relative `optionSources` paths so resolved module and transport defaults retain
49
- their provenance. The snapshot copies only resolved values; the UI consumes
50
- these presence facts separately. Its paths use connector
51
- ids and pool names and are filtered after disclosure; hidden values leave no
52
- provenance keys. It contains no source code or credential material.
53
- Every response is private, `no-store` JSON. Only GET is allowed, with the
54
- same auth gate and identity partition as `/ui/data` and `/ui/connectors/:id`.
55
-
56
- `live.connectors` carries status and problem codes, complete registry tools
57
- with their final read/write classification, catalog age in milliseconds, and
58
- last-call time/outcome. Catalog descriptions and schemas are allowed for an
59
- authenticated reader; grammar-failing names and addresses become `<withheld>`.
60
- `auth.registrationPath` reports the selected downstream OAuth client mechanism when known.
61
- Status prose, error text, credential values or suffixes, arguments, results,
62
- and code are excluded. Static and unobserved catalogs have a null age;
63
- persisted catalogs retain their original fetch time. Probes run with bounded
64
- concurrency and the configured probe deadline, and release their scopes.
65
-
66
- Last-call lookup requires the existing activity permission and read gate. It
67
- scans up to 1,000 recent rows, filters connector/tool grants, and restricts
68
- personal connectors to the caller's activity identity. Each retained fact
69
- copies a validated timestamp and outcome only. A null call means unknown or
70
- absent in that window, not proof of no calls. `live.activity` distinguishes
71
- available, unconfigured, forbidden, and unavailable history. `you` names no
72
- identity or auth material; it reports grants, root and admitted-pool trust,
73
- and effective activity, token, and per-connector auth permissions.
155
+ (`OperatorUiContractV2`) from `@zackbart/connecta/ui`, `schemaVersion: 2`.
156
+ Its declarations ship in `dist/operator-ui/contract.d.ts` in the installed package. Schema
157
+ version 1, `hasGrant`, `you.grants`, and `you.permissions` were removed;
158
+ v2 keeps `you.interactive` to describe whether the session is interactive. It is the page's
159
+ only gate: a signed-out caller gets the auth provider's 401 challenge, and a
160
+ `cta_` agent token the machine-only 401 Bearer challenge every human route
161
+ gives it; a maintainer, or a person listed in a pool's `members`, gets 200
162
+ with `role` `"maintainer"` or `"member"`; anyone else gets the private JSON 404
163
+ `{ "error": "not found" }`. An open deployment's anonymous caller is
164
+ a maintainer. Every response is private, `no-store` JSON, and only GET is
165
+ allowed. One shape serves both roles; a field a role does not receive is
166
+ `null` or empty, never omitted.
167
+
168
+ The maintainer view carries:
169
+
170
+ - `config`, the construction-time `describeConfig()` snapshot, including
171
+ `maintainers` and each pool's `members`, and `configSources`, which labels
172
+ each serialized leaf as `default` or `config` from option presence rather
173
+ than equality with the default. Factory descriptions carry relative
174
+ `optionSources` paths so resolved module and transport defaults retain their
175
+ provenance. Paths use connector ids and pool names and are filtered after
176
+ disclosure; hidden values leave no provenance keys. Neither contains source
177
+ code or credential material.
178
+ - `root`: plain `/mcp`, its `readOnly`, and its `maintainers`, people only.
179
+ - `pools`: every declared pool with `humanPath` (`/mcp/<name>`), `agentPath`
180
+ (`/agent/<name>`), `readOnly`, `owner` (the Workspace address its agents act
181
+ as), its declared `tools` (never intersected; no `requireReadOnly` is ever
182
+ true), and its `members`, agents as `{ kind: "agent", name }`. Every listed
183
+ person carries a `label` from the same directory lookup activity actor
184
+ labels use, on an interactive read only, and `null` without a directory
185
+ (Cloudflare Access) or an entry. An open deployment's anonymous maintainer
186
+ reads every `owner` as `null`.
187
+ - `agents`: every agent declared in code, with its pools, its token slot
188
+ (`state`, `issuedAt`, `revokedAt`, `revision`) exactly as
189
+ `GET /ui/access-tokens` lists it, and `lastSeenAt`, the newest visible
190
+ activity row recorded for that agent, or `null` when activity is off or
191
+ none is visible. A slot read that fails answers 503 rather than drop agents.
192
+ - `matrix`: a cell per connector for plain `/mcp` (`pool: null`) and for each
193
+ pool that serves it, with that endpoint's tool names or `"all"` and
194
+ `runsAs`. A Workspace connector with a subject mapping is
195
+ `{ kind: "workspace", humans: "self", agents }`, `agents` being the pool's
196
+ owner, or `null` on plain `/mcp` and in an ownerless pool; a fixed subject is
197
+ `{ kind: "workspaceFixed", subject }`. A personal connector counts the
198
+ endpoint's people (the maintainers on plain `/mcp`) and how many have
199
+ connected in their own partition: a stored credential of the declared shape,
200
+ or an `ok` status. A shared connector names its `source`: `vault` (a saved
201
+ slot, with `savedAt`), `missing` (a slot with nothing saved, or shared OAuth
202
+ reporting it needs authorizing), `oauth` (a shared grant; grants carry no
203
+ saved time, so `savedAt` is `null`), `unauthenticated` (a remote MCP server
204
+ that declares no auth), or `config` (everything else, held by deployment
205
+ code).
206
+ - `live.connectors`: status and problem codes, complete registry tools with
207
+ their final read/write classification, catalog age in milliseconds, and
208
+ last-call time/outcome. Catalog descriptions and schemas are allowed;
209
+ grammar-failing names and addresses become `<withheld>`.
210
+ `auth.registrationPath` reports the selected downstream OAuth client
211
+ mechanism when known. Static and unobserved catalogs have a null age;
212
+ persisted catalogs retain their original fetch time.
213
+ - `live.activity`: available, unconfigured, forbidden, or unavailable history.
214
+ Last-call and last-seen lookups need the activity permission and read gate,
215
+ scan up to 1,000 recent rows, filter connector/tool grants, restrict personal
216
+ connectors to the caller's activity identity, and copy a validated timestamp
217
+ and outcome only. A null means unknown or absent in that window.
218
+ - `connectors: []`.
219
+
220
+ The member view is exactly that member's projection: `config`,
221
+ `configSources` and `root` are `null`, `live.connectors`, `agents` and
222
+ `matrix` are empty, and `live.activity` is `"forbidden"`. `pools` lists only
223
+ their pools, each with `owner: null` and `members: []`. `connectors` lists
224
+ each connector in those pools with its title, the member's pools that serve
225
+ it, its auth scope, the tools those pools grant with their classification,
226
+ `connection` (their own partition's state and problem code for a personal
227
+ connector, `null` for a shared one) and `manage`. `manage` is derived on the
228
+ server. A shared connector is always `{ kind: "none" }`. For a personal one,
229
+ the member's own permission (an interactive person in a pool that holds it)
230
+ gates every action. A credential slot offers `save` when a vault is
231
+ configured, `clear` when a value is stored in the member's partition, and
232
+ `test` when one is stored in the declared shape and the connector has the
233
+ matching test hook; an unreadable partition offers `save` alone, so the page
234
+ reads stored-ness from `actions`. OAuth offers `connect` and `restart` when
235
+ the connector has `startAuth`, `finishAuth` and `verifyState` and the
236
+ deployment can sign the connect hand-off, and `disconnect` when it has
237
+ `disconnectAuth`. No actions is `{ kind: "none" }`. The view carries no shared
238
+ credential status, catalog ages, last calls, other members, agents, owners or
239
+ activity. Status prose, error text, credential values or suffixes, arguments,
240
+ results, and code are excluded from both views. Probes run with bounded
241
+ concurrency and the configured probe deadline, and release their scopes. Every
242
+ vault, token-slot and activity read the view makes runs under that same
243
+ deadline and the request's signal, so a stalled store never hangs the page: it
244
+ degrades fail-closed, to `save` alone for a member's credential, `missing` or
245
+ an unconnected person in a matrix cell, unavailable activity, and a 503 for
246
+ the token slots.
247
+
248
+ `/ui/data`, `/ui/connectors/:id`, activity reads and access-token routes
249
+ require a maintainer and give anyone else the same private 404. The connector
250
+ auth routes, `/ui/oauth/<id>` and `/ui/credentials/<id>`, also admit a pool
251
+ member for a personal connector in one of their pools, in their own
252
+ partition. `/ui/oauth/<id>` gates each verb on its own hooks: a connect needs
253
+ `startAuth`, `finishAuth` and `verifyState`, a disconnect only
254
+ `disconnectAuth`. `/ui/credentials/<id>` refuses to clear an empty partition,
255
+ the caller's own for a personal connector or the shared slot, with 409
256
+ `{ "error": "no credential is stored to clear", "problem": "credential_required" }`,
257
+ as its test route refuses to test one; a value that cannot be read can still be
258
+ cleared. The routes thus accept what `manage` lists, with one recovery
259
+ exception: clearing a stored value that cannot be read is accepted although
260
+ `manage` lists only `save` in that state. Public HTML shells and static assets carry no catalog data.
261
+ Access-token management also requires an interactive session.
262
+
263
+ `GET /ui/api/config?connector=<id>` narrows the same contract to one connector
264
+ before status and catalog probes. For a maintainer, config, provenance, live
265
+ rows and matrix cells contain only that connector's facts; for a member,
266
+ `connectors` does. Pools keep their declared tools. An id outside the
267
+ caller's view receives a no-store 404. The selector never widens access.
74
268
 
75
269
  `connecta doctor --config` fetches this authenticated contract and prints only
76
- `config` as indented JSON. It requires the UI module and the existing doctor
77
- authentication environment variables; it runs no diagnostic program, follows
78
- no redirects, and prints no raw HTTP failure body.
270
+ a maintainer's `config` as indented JSON; a member's view has none, and doctor
271
+ says the snapshot is for maintainers. It requires the UI module and the
272
+ existing doctor authentication environment variables; it runs no diagnostic
273
+ program, follows no redirects, and prints no raw HTTP failure body.
79
274
 
80
275
  The operator shell uses React, TanStack Router/Query/Table, Radix, cmdk and
81
- Tailwind v4. Inter, CSS, JavaScript and dependency notices are hashed assets
276
+ Tailwind v4. Geist Sans, Geist Mono, CSS, JavaScript and dependency notices are hashed assets
82
277
  under `/ui/assets/*`, served identically with immutable cache headers on Node
83
278
  and Workers. HTML contains only mount points and escaped inert configuration;
84
- it remains uncached. `generated.ts` is ignored and generated before build and
279
+ it remains uncached. Ahead of the stylesheet, every operator page (sign-in
280
+ gate, member page, maintainer pages) loads a blocking hashed scheme initializer
281
+ of under 200 bytes, `src/operator-ui/app/scheme-init.ts`, with a plain
282
+ `<script src>` and no inline code. When the server set no `html[data-scheme]`
283
+ from a pinned `branding.theme.colorScheme`, it copies a saved `light` or `dark`
284
+ from local storage key `connecta:scheme` onto that attribute, ignoring storage
285
+ errors, so the first frame already has the viewer's scheme. The deferred bundle
286
+ learns of a pin from `schemePinned` in the inert configuration; Appearance
287
+ writes the key and moves the attribute live, and is read-only under a pin. Every
288
+ local and session storage access in the UI tolerates storage that throws, as in
289
+ private browsing: a read finds nothing stored and a write keeps nothing, so a
290
+ bearer token is not remembered there. `generated.ts` is ignored and generated before build and
85
291
  test; `check:operator-ui` detects stale assets and shared page styles. The UI
86
292
  stays behind `./ui`, outside the root import graph. The identity-fenced store
87
- owns authenticated reads and existing mutations. Overview, Connectors, connector
88
- detail, Tools, Access and Config consume the typed contract. Activity retains
293
+ owns authenticated reads and existing mutations. An Access-only shell selects
294
+ Access even when the HTML request has no trusted context; only authenticated
295
+ API responses establish the session. A gated Access session offers Retry.
296
+ Focus, visibility, and timer-driven recovery share one admission-read schedule
297
+ with at least two seconds between passive attempts. An admission read takes
298
+ the contract first and, for a maintainer, then `/ui/data` and the connector
299
+ details, which reuse that contract rather than read it again. An in-flight
300
+ passive attempt coalesces returns until its admission read finishes, so slow
301
+ successful reads can publish. Each admission read shares one 15-second
302
+ deadline, including token acquisition, fetches and response bodies. This is an admission deadline, not a request
303
+ lifetime limit: it releases the recovery guard and exposes Retry without aborting
304
+ the pending read or invalidating its identity/revision fence. A late healthy
305
+ response still publishes if that cycle is current. Passive timers and returns
306
+ coalesce onto the pending cycle, including after its deadline; they never start
307
+ duplicate summary, token, config, detail or body reads. The passive retry schedule
308
+ stays bounded and does not reset its budget on a deadline. Retry remains available
309
+ during recovery configuration loading and supersedes and aborts the old cycle;
310
+ identity changes, navigation and connector-page unmount also cancel it. Navigation
311
+ that cancels pending data schedules a bounded reread for the destination. Failed reads retry after
312
+ 2, 2, 4, and 8 seconds. Refusals preserve the throttle and retry budget. After
313
+ those four automatic retries, a focus return can request one more read after
314
+ the eight-second backoff, without replenishing the automatic budget. Retry
315
+ resets the budget with a 150 ms click debounce. Successful reads and explicit
316
+ identity transitions also reset recovery. Identity changes
317
+ cancel pending retries and fence out responses from the previous identity.
318
+ Queued focus callbacks carry the same identity fence. Explicit sign-out
319
+ suppresses passive reads until an intentional sign-in or a new Clerk session.
320
+
321
+ Catalog and connector auth details share one revisioned refresh cycle. Each
322
+ completed connector publishes its catalog facts and auth controls together,
323
+ while a slow provider leaves other rows usable.
324
+ Catalog refresh, connector refresh, and mutation follow-up reads update both
325
+ consumers together. A later refresh or mutation invalidates an older cycle's publication fence
326
+ without aborting its read, and a failed contract read clears its old snapshot;
327
+ the next refresh then reads the contract before any connector detail. After an explicit OAuth
328
+ start, visible tabs poll only that connector's read-only catalog and auth facts
329
+ every two seconds for up to 30 seconds. The scoped result replaces only that
330
+ connector's matching live row and auth controls. Success, a failed read or status,
331
+ disconnect, navigation away from the connector, unmount, or identity reset ends
332
+ polling and aborts its in-flight reads. The deadline also aborts a stalled read.
333
+ A later focus return can request another status read. Recovery never starts OAuth
334
+ or replays a mutation.
335
+ Unreadable credential metadata uses `configured: null`, including named fields,
336
+ and renders as unavailable with a retry prompt. It does not claim that the slot
337
+ is empty. Endpoints, Connectors, connector
338
+ detail, Settings, and the member page consume the typed contract; Activity opens
339
+ only to a maintainer whose contract reports readable history. Connector detail
340
+ reads auth permissions from its `/ui/data` connector. Activity retains
89
341
  its authenticated data routes. Connector detail tabs live
90
342
  in the URL hash; Activity filters live in the query string and apply to loaded
91
- history. Both `/ui/activity` and `/ui/api/activity` require Activity access and
92
- pass the optional activity `readGate`. Interactive operators and machine callers
93
- admitted by `identity.activityAccess` share the same history disclosure rules.
343
+ history. Both `/ui/activity` and `/ui/api/activity` require a maintainer
344
+ principal, interactive or not, and pass the optional activity `readGate`.
345
+ Interactive and machine maintainer reads share the same history disclosure rules.
94
346
  Connector/tool grants and grants for the recorded pool filter each event. Shared
95
347
  connector history can include other principals' events, with recorded request
96
348
  IDs, validated client name/version and package version, and actor kind, ID and
@@ -112,12 +364,31 @@ id and typed actor when available, otherwise a fresh id and system actor. No
112
364
  names, descriptions or schemas enter the event. Private catalog changes require
113
365
  a matching principal owner even when the connector uses shared auth. Empty credential declarations
114
366
  report `credential_required` before running a connector status or catalog probe.
115
- The Access adapter reports inbound provider kinds, admitted pools, grants, trust
116
- and endpoint setup; token controls retain their existing permission gate.
367
+ The top bar shows deployment branding and a name breadcrumb, then Search (the
368
+ command palette), Appearance, and the session action (Change token or Sign out)
369
+ at one quiet size; a member's top bar has only branding, the account and the
370
+ session action. The tab row lists Endpoints, Connectors, Activity,
371
+ Tokens, and Settings. Activity and Tokens keep their permission gates; a gated
372
+ viewer sees no operator navigation. The command palette searches visible pages,
373
+ connectors and tools, opening connector results on Tools. `/access` redirects
374
+ to `/`, `/tools` to `/connectors`, and `/config` to `/settings` in the browser
375
+ with history replacement. Connector hashes `#config`, `#auth` and `#diagnostics`
376
+ redirect to `#connection`, preserving query parameters such as signed handoffs.
377
+ Token controls retain their existing permission gate.
117
378
  Configuration remains in deployment code. The UI mutates only credentials,
118
- OAuth connections and client tokens through the existing routes.
379
+ OAuth connections and agent tokens through the existing routes.
380
+
381
+ ## Contributor reference
382
+
383
+ ### Source and tests
119
384
 
120
- ## Source and tests
385
+ The browser store reads only v2 and checks `role` before reading a
386
+ maintainer-only field. `test/fixtures/operator-contract-v2.ts` holds
387
+ maintainer and member fixtures, and `test/helpers/operator-member-view.ts`
388
+ checks a member response's exact shape and compares its values with a
389
+ projection built from config, membership, the member's own partition status,
390
+ and each personal connector's exact `manage`;
391
+ `test/operator-contract-server.test.ts` runs it against real responses.
121
392
 
122
393
  The contract type is [contract.ts](https://github.com/zackbart/connecta/blob/main/src/operator-ui/contract.ts).
123
394
  [UI routes](https://github.com/zackbart/connecta/blob/main/src/routes/ui.ts) and
@@ -2,17 +2,14 @@
2
2
 
3
3
  Use this upgrade guide when moving a Stripe, Notion, Vercel, or Cloudflare
4
4
  deployment from 0.30 to 0.31, where `auth` selects each provider's
5
- implementation ([#801](https://github.com/zackbart/connecta/issues/801)).
6
- The [0.31.0 release notes](https://github.com/zackbart/connecta/blob/main/release-notes/0.31.0.md) summarize the change.
5
+ implementation.
7
6
  [Deployment upgrades](./deploying.md#upgrade-an-existing-deployment) cover other
8
7
  version changes; [integrating services](./integrating.md) covers new connectors.
9
8
 
10
9
  A dual provider requires `auth`. `{ type: "oauth" }` connects the vendor's
11
10
  hosted MCP server, OAuth only. A key type connects Connecta's REST connector
12
11
  with an operator-managed credential. There is no default, and one connector id
13
- uses one implementation: configure two ids for both. The
14
- [decision record](https://github.com/zackbart/connecta/blob/main/decisions/0005-auth-selects-implementation.md)
15
- explains why.
12
+ uses one implementation: configure two ids for both.
16
13
 
17
14
  ## Stripe
18
15
 
@@ -62,6 +59,10 @@ contracts: search finds operations in a pinned API index, every call is
62
59
  checked against that index before it is sent, results arrive as
63
60
  `{ status, data, page? }`, and POST writes send the caller's `idempotencyKey`
64
61
  or a generated one as `Idempotency-Key` and return it (DELETE sends none).
62
+ Caller-supplied keys may contain up to 255 characters. An ambiguous POST write
63
+ also returns the key in `uncertainCall.recovery.idempotencyKey`, including when
64
+ the call deadline interrupts it. See [argument recovery](./meta-tools.md#argument-recovery)
65
+ before retrying.
65
66
  Every request, v1 and v2, sends `Stripe-Version: 2026-09-30.endive`, the
66
67
  version of the pinned index, whatever the account's default API version, so
67
68
  response shapes and accepted parameters follow that version. Check fields
@@ -73,9 +74,7 @@ A key whose `sk_live_`/`rk_live_` or `sk_test_`/`rk_test_` prefix contradicts
73
74
  Lost on the key path: natural-language Sigma (`stripe_analytics`), metrics,
74
75
  documentation search, the implementation planner, reports, and feedback exist
75
76
  only on the hosted OAuth connection. Programs calling those tools on a key
76
- connector need an OAuth connector beside it. The
77
- [Stripe skill](https://github.com/zackbart/connecta/blob/main/src/providers/stripe/SKILL.md)
78
- owns the conventions for both.
77
+ connector need an OAuth connector beside it. Read the connector runtime guide with `connecta.skill("connector:<id>")` in `execute_code` for its usage conventions.
79
78
 
80
79
  ## Notion
81
80
 
@@ -101,6 +100,15 @@ connectors: [
101
100
  | `integration_list_comments`, `integration_add_comment` | `notion_api_read` `GET /v1/comments`; `notion_api_write` `POST /v1/comments`. |
102
101
  | `integration_update_page_properties`, `integration_trash_page` | `notion_api_write` `PATCH /v1/pages/{page_id}` with `properties`, or `in_trash` to trash or restore. |
103
102
 
103
+ Both auth variants accept optional `callAdmission`. On a token connector it
104
+ replaces the whole default policy of 180 calls per minute and 3 concurrent
105
+ per runtime. Malformed policies throw at construction. Notion's
106
+ [request limits](https://developers.notion.com/reference/request-limits) allow
107
+ 600 requests per minute per connection on Business and Enterprise and 180 on
108
+ other plans, with a separate workspace-wide limit shared by all connections.
109
+ Account for other runtimes and tools that make multiple HTTP requests when
110
+ choosing an override.
111
+
104
112
  The token connector gains Connecta's generic REST tools over Notion's pinned
105
113
  API index (`notion_api_search`, `notion_api_details`, `notion_api_read`,
106
114
  `notion_api_write`), so every public operation is reachable with the bot's
@@ -119,8 +127,7 @@ duplication, team and skill search, attachment downloads, or page-to-skill
119
127
  conversion. An OAuth connector acts as the user, and has none of the generic
120
128
  REST tools or the `integration_*` projections and authoring helpers. Configure
121
129
  both ids when a deployment needs both; a failed call is never retried on the
122
- other. The [Notion skill](https://github.com/zackbart/connecta/blob/main/src/providers/notion/SKILL.md)
123
- owns the conventions for both.
130
+ other. Read the connector runtime guide with `connecta.skill("connector:<id>")` in `execute_code` for its usage conventions.
124
131
 
125
132
  ## Vercel
126
133
 
@@ -156,7 +163,7 @@ variable tools (`list_project_env_vars`, `upsert_project_env_var`,
156
163
  (the runtime stream, stopped after `waitMs` or `maxRows`), and
157
164
  `vercel_api_upload`.
158
165
 
159
- Value safety follows a reviewed table (`src/providers/vercel/value-safety.ts`).
166
+ Value safety follows a reviewed table bundled with the connector.
160
167
  Every operation whose response schema in the pinned spec may carry a credential
161
168
  or a stored secret has a verdict: refused (credential minting, Connect
162
169
  authorization, decrypted environment values, Global Config items and tokens,
@@ -183,12 +190,10 @@ connector beside the token connector.
183
190
 
184
191
  The hosted connection now lists `filter_project_envs`, `get_project_env`,
185
192
  `create_project_env`, and `edit_project_env`, which 0.29 hid. The two reads can
186
- return decrypted values, so they are reviewed as writes and the pool trust
187
- policy gates them. Vercel admits only reviewed and approved MCP clients to its
193
+ return decrypted values, so they are reviewed as writes, and a `readOnly`
194
+ endpoint refuses them. Vercel admits only reviewed and approved MCP clients to its
188
195
  hosted server; if authorization is refused for that reason, the deployment
189
- needs Vercel's approval or a token connector. The
190
- [Vercel skill](https://github.com/zackbart/connecta/blob/main/src/providers/vercel/SKILL.md)
191
- owns the conventions for both.
196
+ needs Vercel's approval or a token connector. Read the connector runtime guide with `connecta.skill("connector:<id>")` in `execute_code` for its usage conventions.
192
197
 
193
198
  ## Cloudflare
194
199
 
@@ -245,17 +250,16 @@ hooks) and redacts stored secrets on every result. A pinned connector refuses
245
250
  operations that name no account or zone unless they are reviewed safe, such
246
251
  as `/certificates` and `/memberships/{id}`.
247
252
 
248
- Read-only pools: hosted `execute` is always a write, even for a program that
249
- only reads, so a read-only pool on an OAuth connector reaches only `search`.
253
+ Read-only endpoints: hosted `execute` is always a write, even for a program
254
+ that only reads, so a `readOnly` endpoint on an OAuth connector reaches only
255
+ `search`.
250
256
  Use an API-token connector for read-only API access.
251
257
 
252
258
  Lost on each path. OAuth has no index validation, named tools, GraphQL tool,
253
259
  upload tool, or pin; Cloudflare's OAuth grant is the only boundary. A key has
254
260
  no hosted `search` over Cloudflare's live OpenAPI document; the pinned index
255
261
  replaces it and `providers:check` reports when it falls behind. A Global API
256
- Key cannot reach R2, which accepts API tokens only. The
257
- [Cloudflare skill](https://github.com/zackbart/connecta/blob/main/src/providers/cloudflare/SKILL.md)
258
- owns the conventions for both.
262
+ Key cannot reach R2, which accepts API tokens only. Read the connector runtime guide with `connecta.skill("connector:<id>")` in `execute_code` for its usage conventions.
259
263
 
260
264
  ## Value safety on every key connector
261
265
 
@@ -288,12 +292,16 @@ one; read or rotate withheld secrets in the vendor's dashboard.
288
292
  Failures of credential-bearing operations keep Stripe's type, code, and
289
293
  param but not its message. Create client-side sessions and links from your
290
294
  own server code.
291
- - **Notion** results withhold the signature, credential, and security-token
292
- parameters of pre-signed file URLs, so a returned file URL no longer
293
- downloads; credential-named fields, including database properties named
294
- like credentials, read as `"[redacted]"`. Failures of the OAuth, file
295
- upload, bot user, and agent operations keep Notion's code but not its
296
- message.
295
+ - **Notion** REST results and error text withhold the whole query and
296
+ fragment of canonical Notion file links, including links embedded in
297
+ Markdown and async task results. External proxy URLs keep their bytes;
298
+ AWS and Google signing-parameter values are withheld at any host. Writes
299
+ carrying the plain or encoded `[redacted]` placeholder in a Notion file
300
+ link's own query or fragment are refused before dispatch. See the
301
+ connector runtime guide through `connecta.skill("connector:<id>")` in `execute_code` for recognized hosts and scanning limits. Credential-named fields,
302
+ including database properties named like credentials, read as
303
+ `"[redacted]"`. Failures of the OAuth, file upload, bot user, and agent
304
+ operations keep Notion's code but not its message.
297
305
  - **Vercel** replaces environment values with `"[redacted]"` instead of
298
306
  dropping the field, and withholds Vercel's error text for every reviewed
299
307
  operation and every operation whose request takes a credential-named
@@ -305,3 +313,10 @@ one; read or rotate withheld secrets in the vendor's dashboard.
305
313
  destinations to their scheme and host. A credential-named field now loses
306
314
  its whole value, not only its strings (a `cookies` list, a variable named
307
315
  `API_KEY`).
316
+
317
+ ## Contributor reference
318
+
319
+ [Decision 0005](https://github.com/zackbart/connecta/blob/main/decisions/0005-auth-selects-implementation.md)
320
+ explains why auth selects the implementation. Maintained provider source
321
+ skills and review tables live under `src/providers/` in the repository;
322
+ `src/providers/vercel/value-safety.ts` owns Vercel's reviewed table.
@@ -7,8 +7,8 @@ version changes; [integrating services](./integrating.md) covers new connectors.
7
7
 
8
8
  This is the Phase 1 item 5d migration input for [Phase 5 release #709](https://github.com/zackbart/connecta/issues/709).
9
9
  Deploy it with the consolidated 0.29 release, not while the rework is in progress.
10
- BePresent and One&Many use Notion and Cloudflare; their actual grants and
11
- connector ids must be checked in deployment configuration before that release.
10
+ Deployments that use Notion or Cloudflare must check their actual grants and
11
+ connector ids in deployment configuration before that release.
12
12
  The provider audit establishes available auth modes, not production OAuth grants.
13
13
 
14
14
  Cloudflare's part of this guide is superseded: `cloudflare()` now selects its
@@ -58,7 +58,7 @@ complement. Replace `update_r2_bucket` with `cloudflare_api_mutate` using
58
58
  `cf-r2-storage-class` set to `Standard` or `InfrequentAccess`. Omit the body;
59
59
  include `cf-r2-jurisdiction` only when the bucket needs it. This remains a write.
60
60
  `execute` remains a write even when its program only calls GET.
61
- Trusted pools may execute writes; read-only pools use direct host-approved writes.
61
+ Writable endpoints may run `execute`; `readOnly` endpoints refuse it.
62
62
 
63
63
  Vercel's `surface` and ownership rules were later replaced: `auth` now selects
64
64
  the implementation. See the [provider auth migration](./provider-auth-migration.md#vercel).