@zackbart/connecta 0.40.0 → 0.42.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 (195) hide show
  1. package/CHANGELOG.md +148 -0
  2. package/bin/connecta.mjs +26 -17
  3. package/bin/meta-tool-names.d.mts +3 -1
  4. package/bin/meta-tool-names.mjs +4 -1
  5. package/bin/version.mjs +1 -1
  6. package/dist/activity.d.ts +32 -5
  7. package/dist/activity.js +32 -6
  8. package/dist/advertised-schema.js +15 -0
  9. package/dist/agent-observation.js +24 -2
  10. package/dist/agent-routes.js +33 -44
  11. package/dist/auth/clerk.d.ts +6 -1
  12. package/dist/auth/clerk.js +16 -0
  13. package/dist/auth/cloudflare-access.js +49 -7
  14. package/dist/auth/downstream-oauth/consent.js +79 -29
  15. package/dist/auth/downstream-oauth/grant-store.js +58 -7
  16. package/dist/auth/downstream-oauth/refresh-coordinator.js +4 -1
  17. package/dist/auth/downstream-oauth/refresh-records.js +6 -5
  18. package/dist/auth/downstream-oauth.js +15 -4
  19. package/dist/auth/oauth-request-not-sent.js +4 -0
  20. package/dist/auth/static-oauth.js +77 -39
  21. package/dist/auth-elicitation.js +118 -61
  22. package/dist/authorization-handoff.js +36 -3
  23. package/dist/catalog-cache.js +352 -43
  24. package/dist/catalog-freshness.js +25 -0
  25. package/dist/catalog-service.js +408 -78
  26. package/dist/catalog.js +216 -62
  27. package/dist/config-defaults.d.ts +2 -1
  28. package/dist/config-defaults.js +4 -1
  29. package/dist/config.d.ts +53 -21
  30. package/dist/config.js +56 -17
  31. package/dist/connectors/remote-mcp/call.js +1 -1
  32. package/dist/connectors/remote-mcp/catalog.js +52 -28
  33. package/dist/connectors/remote-mcp/connection.js +113 -22
  34. package/dist/connectors/remote-mcp/oauth.js +2 -2
  35. package/dist/connectors/remote-mcp/resources.js +5 -2
  36. package/dist/connectors/remote-mcp/transport.js +24 -6
  37. package/dist/connectors/remote-mcp.js +14 -5
  38. package/dist/credential-contract.d.ts +3 -1
  39. package/dist/describe-config.d.ts +4 -2
  40. package/dist/describe-config.js +1 -0
  41. package/dist/discovery-health.js +26 -11
  42. package/dist/downstream-input.js +11 -2
  43. package/dist/errors.d.ts +60 -4
  44. package/dist/errors.js +88 -3
  45. package/dist/execute.js +491 -202
  46. package/dist/executor-admission.js +2 -0
  47. package/dist/executor-contract.js +6 -0
  48. package/dist/executor-result.js +3 -3
  49. package/dist/executors/quickjs-child.js +14 -5
  50. package/dist/executors/quickjs-runtime.d.ts +2 -0
  51. package/dist/executors/quickjs-runtime.js +37 -20
  52. package/dist/executors/quickjs.js +24 -7
  53. package/dist/guarded-transport.js +2 -0
  54. package/dist/guest-functions.js +5 -4
  55. package/dist/guest-runtime.js +37 -1
  56. package/dist/guest-types.d.ts +18 -0
  57. package/dist/http/client.js +14 -2
  58. package/dist/http/errors.js +9 -4
  59. package/dist/http/fields.js +17 -5
  60. package/dist/http/hatch.js +11 -3
  61. package/dist/http/node-transport.js +110 -4
  62. package/dist/http/oauth.js +17 -6
  63. package/dist/http/paging.js +1 -1
  64. package/dist/http/provider.js +2 -6
  65. package/dist/http/vendor.js +6 -1
  66. package/dist/index.d.ts +1 -1
  67. package/dist/index.js +15 -21
  68. package/dist/invocation-failure.js +28 -0
  69. package/dist/invocation.js +167 -64
  70. package/dist/membership.d.ts +1 -1
  71. package/dist/meta-output.js +24 -18
  72. package/dist/meta-tool-names.d.ts +1 -1
  73. package/dist/meta-tool-names.js +1 -1
  74. package/dist/meta-tools.js +286 -73
  75. package/dist/module-contracts.d.ts +3 -1
  76. package/dist/native-recovery.js +4 -1
  77. package/dist/native-tools.js +177 -55
  78. package/dist/node.d.ts +7 -0
  79. package/dist/node.js +85 -4
  80. package/dist/oauth-handoff.js +53 -43
  81. package/dist/operation-activity.js +104 -0
  82. package/dist/operation-vocabulary.js +6 -0
  83. package/dist/operator-record.d.ts +9 -1
  84. package/dist/operator-record.js +16 -0
  85. package/dist/operator-ui/generated.js +2 -2
  86. package/dist/program-result.js +2 -0
  87. package/dist/program-source.js +1 -1
  88. package/dist/program-writes.js +112 -19
  89. package/dist/provider.d.ts +1 -14
  90. package/dist/provider.js +0 -3
  91. package/dist/providers/_shared/google/kit.js +2 -2
  92. package/dist/providers/_shared/google/workspace.d.ts +1 -1
  93. package/dist/providers/breeze/api.js +2 -2
  94. package/dist/providers/breeze/index.js +1 -1
  95. package/dist/providers/breeze/skill.generated.js +1 -1
  96. package/dist/providers/ccb/api.js +2 -2
  97. package/dist/providers/ccb/index.js +1 -1
  98. package/dist/providers/ccb/skill.generated.js +1 -1
  99. package/dist/providers/cloudflare/api.js +2 -2
  100. package/dist/providers/cloudflare/index.js +3 -1
  101. package/dist/providers/cloudflare/skill.generated.js +1 -1
  102. package/dist/providers/docs/index.js +1 -1
  103. package/dist/providers/docs/skill.generated.js +1 -1
  104. package/dist/providers/drive/index.js +1 -1
  105. package/dist/providers/drive/skill.generated.js +1 -1
  106. package/dist/providers/forms/index.js +2 -1
  107. package/dist/providers/forms/skill.generated.js +1 -1
  108. package/dist/providers/github/api.js +98 -6
  109. package/dist/providers/github/index.js +8 -3
  110. package/dist/providers/github/lib/auth.d.ts +3 -1
  111. package/dist/providers/github/lib/auth.js +43 -12
  112. package/dist/providers/github/lib/discovery.js +11 -1
  113. package/dist/providers/github/lib/operations.js +8 -0
  114. package/dist/providers/github/safety.js +39 -0
  115. package/dist/providers/github/skill.generated.js +1 -1
  116. package/dist/providers/github/tools.js +12 -5
  117. package/dist/providers/gmail/index.js +2 -1
  118. package/dist/providers/gmail/skill.generated.js +1 -1
  119. package/dist/providers/granola/index.js +1 -1
  120. package/dist/providers/granola/skill.generated.js +1 -1
  121. package/dist/providers/overflow/index.js +3 -1
  122. package/dist/providers/overflow/skill.generated.js +1 -1
  123. package/dist/providers/planning-center/index.js +12 -6
  124. package/dist/providers/planning-center/lib/constants.d.ts +1 -1
  125. package/dist/providers/planning-center/skill.generated.js +1 -1
  126. package/dist/providers/quickbooks/api.js +28 -25
  127. package/dist/providers/quickbooks/index.js +3 -6
  128. package/dist/providers/quickbooks/lib/writes.js +55 -0
  129. package/dist/providers/quickbooks/safety.js +17 -5
  130. package/dist/providers/quickbooks/skill.generated.js +1 -1
  131. package/dist/providers/sheets/index.js +2 -1
  132. package/dist/providers/sheets/skill.generated.js +1 -1
  133. package/dist/providers/slides/index.js +3 -3
  134. package/dist/providers/slides/skill.generated.js +1 -1
  135. package/dist/providers/stripe/index.js +2 -2
  136. package/dist/providers/stripe/skill.generated.js +1 -1
  137. package/dist/providers/tithely/index.js +2 -1
  138. package/dist/providers/tithely/skill.generated.js +1 -1
  139. package/dist/providers/vercel/api.js +3 -2
  140. package/dist/providers/vercel/skill.generated.js +1 -1
  141. package/dist/registry.d.ts +36 -4
  142. package/dist/registry.js +142 -30
  143. package/dist/resource-read.js +21 -2
  144. package/dist/result-preview.js +210 -0
  145. package/dist/result-seal.js +168 -0
  146. package/dist/route-names.js +53 -0
  147. package/dist/routes/activity-context.js +1 -0
  148. package/dist/routes/activity-disclosure.js +26 -4
  149. package/dist/routes/activity.js +2 -1
  150. package/dist/routes/connect.js +21 -13
  151. package/dist/routes/mcp.js +83 -35
  152. package/dist/routes/shared.d.ts +6 -0
  153. package/dist/routes/ui-config.js +1 -1
  154. package/dist/routes/ui-data.js +3 -2
  155. package/dist/runtime/call.js +15 -10
  156. package/dist/runtime/native-storage.js +12 -0
  157. package/dist/runtime/request-reads.js +66 -14
  158. package/dist/runtime/request-scope.js +38 -2
  159. package/dist/runtime/run.js +4 -1
  160. package/dist/sent-secrets.d.ts +20 -0
  161. package/dist/sent-secrets.js +49 -1
  162. package/dist/skill-origin.js +16 -0
  163. package/dist/skills.js +213 -57
  164. package/dist/storage/d1-driver.js +33 -5
  165. package/dist/storage/keys.js +16 -7
  166. package/dist/storage/sql-native.js +60 -3
  167. package/dist/storage/sql.js +6 -0
  168. package/dist/timing-trace.js +84 -0
  169. package/dist/tool-safety.d.ts +33 -0
  170. package/dist/tool-safety.js +51 -15
  171. package/dist/types.d.ts +36 -0
  172. package/dist/url-safety.js +38 -20
  173. package/dist/usage-guide.js +14 -13
  174. package/dist/utility-recovery.js +33 -0
  175. package/dist/validate.js +29 -10
  176. package/dist/version.d.ts +1 -1
  177. package/dist/version.js +1 -1
  178. package/dist/worker.js +60 -15
  179. package/documentation/architecture.md +152 -43
  180. package/documentation/auth.md +160 -53
  181. package/documentation/code-mode.md +396 -119
  182. package/documentation/deploying.md +35 -10
  183. package/documentation/hosted-mcp-migration.md +9 -3
  184. package/documentation/integrating.md +27 -5
  185. package/documentation/meta-tools.md +761 -157
  186. package/documentation/operating.md +91 -4
  187. package/documentation/operator-ui.md +51 -2
  188. package/documentation/providers.md +34 -20
  189. package/examples/worker/README.md +3 -3
  190. package/guides/how-it-works.md +2 -1
  191. package/guides/safety-and-trust.md +1 -3
  192. package/package.json +10 -8
  193. package/templates/node/AGENTS.md +3 -3
  194. package/templates/node/README.md +1 -1
  195. package/templates/node/package.json +1 -1
@@ -295,7 +295,7 @@ It has no queue and no configuration option. When full, a request receives HTTP
295
295
  `error: { code: "authentication_busy", message: "Authentication capacity reached.", retryable: true }`.
296
296
  The operator log records only the checked `authentication_busy` reason, never
297
297
  credentials or identity-provider diagnostics. Retry after the indicated delay;
298
- Connecta does not automatically retry the authentication.
298
+ Connecta does not automatically retry authentication, except for one rejected Cloudflare Access identity lookup inside its existing deadline (see [Access on Workers](#cloudflare-access-on-workers)).
299
299
 
300
300
  Underlying work stays charged until it settles, including cancellation cleanup.
301
301
  A caller leaving alone never releases capacity. After an authorization's hard
@@ -399,7 +399,9 @@ This bound is independent of MCP request/program admission and downstream call
399
399
  limits. `Mcp-Method: subscriptions/listen` still defers its MCP request permit:
400
400
  after authentication and endpoint admission, a body-confirmed modern listen is
401
401
  refused promptly even when execution capacity is full. It always passes the
402
- authentication bound, and no request body is read before authentication.
402
+ authentication bound, and the MCP route reads no request body before
403
+ authentication. On Node, `listen()` has already buffered the body up to its own
404
+ [pre-auth cap](./deploying.md#start-on-node) before core sees the request.
403
405
 
404
406
  Routes that do not invoke an inbound verifier consume no authentication slot:
405
407
  health, public OAuth discovery and client metadata, UI shells and static assets,
@@ -558,6 +560,7 @@ createConnecta({
558
560
  storage,
559
561
  agents: ["assistant", "ci"],
560
562
  accessTokens: accessTokens(storage),
563
+ publicUrl: "https://connecta.example.com",
561
564
  auth: clerkAuth({ publishableKey, secretKey }),
562
565
  ui: operatorUi(),
563
566
  maintainers: ["your-operator-id"],
@@ -948,6 +951,7 @@ cache entries remain partitioned by the admitted pool.
948
951
  createConnecta({
949
952
  storage,
950
953
  accessTokens: accessTokens(storage),
954
+ publicUrl: "https://connecta.example.com",
951
955
  auth: clerkAuth({ publishableKey, secretKey }),
952
956
  readOnly: false, // required; pools without their own readOnly inherit it
953
957
  maintainers: ["user_owner"],
@@ -971,19 +975,19 @@ warning says, every pool path answers the 404 because nobody can be a member,
971
975
  and every agent endpoint answers the machine-only 401 because no agent can be
972
976
  admitted.
973
977
 
974
- | Rule | Contract |
975
- | ------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
976
- | Scope | `/mcp/<pool>` and `/agent/<pool>` serve exactly the pool's `tools` (connector ids or exact `connector.tool` addresses), with the pool's `readOnly`, to its member people and member agents respectively. Plain `/mcp` serves the whole catalog to maintainers and the unique member pool to other people. Neither intersects with or widens anything. |
977
- | Refusal | A person with zero or several memberships at `/mcp`, a non-member, and an undeclared pool get an identical 404 status, body, and headers, after auth. The operator log records `not_maintainer` (zero memberships), `ambiguous_pool` (several memberships), `not_pool_member`, or `pool_not_declared`. Names are not anonymously enumerable; valid credentials cannot distinguish these cases by content. |
978
- | Agents | An agent member reaches its pools only at `/agent/<pool>`, with its `cta_` token. Human endpoints refuse `cta_`; see [agent endpoints](#agent-endpoints). |
979
- | Surface | From 0.35.0, optional `surface`: `"meta"` (default) serves the six meta-tools; `"native"` lists each of the pool's downstream tools directly, for clients with their own tool search, with optional `executeCode: false` to replace `execute_code` with `read_result` (invalid on meta pools). Calls take the same paths either way. See the [native tool surface](./meta-tools.md#native-tool-surface). |
980
- | Description | Optional `description`: plain text telling the model what this endpoint is for. `/mcp/<pool>` and `/agent/<pool>` append it to the MCP `initialize` instructions after `This endpoint: `; a maintainer at plain `/mcp` and a pool without one serve the shared instructions unchanged. A routed person receives their pool description. It grants, hides, and refuses nothing, and changes no tool or tool description. |
981
- | Construction | Malformed names, unknown options or connectors, empty pools, empty or missing `members`, mixed people and agent membership (a `ConfigError` naming the pool), unresolvable member references, a non-boolean `readOnly`, an `owner` that is not a Workspace email address, a `description` that is not 1 to 500 characters of one-line text after trimming, a retired `trust` or `grant`, and addresses absent from an `api()` static catalog throw. Remote catalogs are checked at lazy load. |
982
- | Discovery | Clerk's pool 401 names `/.well-known/oauth-protected-resource/mcp/<pool>` with the pool URL as `resource`, matching RFC 9728. Cloudflare Managed OAuth is application-level and needs no pool setup. |
983
- | Drift | No wildcards; pool tools are exact names. Missing live tools are unreachable and warned once while in a bounded 1,024-entry FIFO. |
978
+ | Rule | Contract |
979
+ | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
980
+ | Scope | `/mcp/<pool>` and `/agent/<pool>` serve exactly the pool's `tools` (connector ids or exact `connector.tool` addresses), with the pool's `readOnly`, to its member people and member agents respectively. Plain `/mcp` serves the whole catalog to maintainers and the unique member pool to other people. Neither intersects with or widens anything. |
981
+ | Refusal | A person with zero or several memberships at `/mcp`, a non-member, and an undeclared pool get an identical 404 status, body, and headers, after auth. The operator log records `not_maintainer` (zero memberships), `ambiguous_pool` (several memberships), `not_pool_member`, or `pool_not_declared`. Names are not anonymously enumerable; valid credentials cannot distinguish these cases by content. |
982
+ | Agents | An agent member reaches its pools only at `/agent/<pool>`, with its `cta_` token. Human endpoints refuse `cta_`; see [agent endpoints](#agent-endpoints). |
983
+ | Surface | From 0.35.0, optional `surface`: `"meta"` (default) serves the meta-tools; `"native"` lists each of the pool's downstream tools directly, for clients with their own tool search, with optional `executeCode: false` to replace `execute_code` with `read_result` (invalid on meta pools). Calls take the same paths either way. See the [native tool surface](./meta-tools.md#native-tool-surface). |
984
+ | Description | Optional `description`: plain text telling the model what this endpoint is for. `/mcp/<pool>` and `/agent/<pool>` place the description before the rest of the MCP instructions; a maintainer at plain `/mcp` and a pool without one serve none. A routed person receives their pool description. It grants, hides, and refuses nothing, and changes no tool or tool description. |
985
+ | Construction | Malformed names, unknown options or connectors, empty pools, empty or missing `members`, mixed people and agent membership (a `ConfigError` naming the pool), unresolvable member references, a non-boolean `readOnly`, an `owner` that is not a Workspace email address, a `description` that is not 1 to 500 UTF-16 code units of one-line text after trimming, a retired `trust` or `grant`, and addresses absent from an `api()` static catalog throw. Remote catalogs are checked at lazy load. |
986
+ | Discovery | Clerk's pool 401 names `/.well-known/oauth-protected-resource/mcp/<pool>` with the pool URL as `resource`, matching RFC 9728. Cloudflare Managed OAuth is application-level and needs no pool setup. |
987
+ | Drift | No wildcards; pool tools are exact names. Missing live tools are unreachable and warned once while in a bounded 1,024-entry FIFO. |
984
988
 
985
989
  Pool descriptions are trimmed before validation. The remaining text must be
986
- 1 to 500 characters and contain no C0 or C1 control characters (including tabs,
990
+ 1 to 500 UTF-16 code units (non-BMP characters such as emoji count as two units) and contain no C0 or C1 control characters (including tabs,
987
991
  newlines, and NEL U+0085), LINE SEPARATOR (U+2028), or PARAGRAPH SEPARATOR
988
992
  (U+2029).
989
993
 
@@ -1102,6 +1106,7 @@ const team = [
1102
1106
  { handle: "bob", id: "user_bob", email: "bob@example.com" },
1103
1107
  ];
1104
1108
  createConnecta({
1109
+ publicUrl: "https://connecta.example.com",
1105
1110
  auth: clerkAuth({ publishableKey, secretKey }),
1106
1111
  storage,
1107
1112
  accessTokens: accessTokens(storage),
@@ -1250,6 +1255,7 @@ const workspace: GoogleWorkspaceOptions = {
1250
1255
  };
1251
1256
 
1252
1257
  createConnecta({
1258
+ publicUrl: "https://connecta.example.com",
1253
1259
  auth: clerkAuth({ publishableKey, secretKey }),
1254
1260
  storage,
1255
1261
  accessTokens: accessTokens(storage),
@@ -1364,21 +1370,34 @@ Each owner of an OAuth connector (the deployment when shared, each principal
1364
1370
  when personal) has one **grant record**, `oauth:grant` in the connector's
1365
1371
  namespace: the live **epoch**, the latest consent's state digest, and one
1366
1372
  authorization server's client registration, tokens, and discovery. Each
1367
- consent has a **flow record**, `oauth:flow:<sha256(state)>`, holding its
1368
- epoch, consent URL, and PKCE verifier for fifteen minutes; the state itself is
1369
- stored nowhere.
1373
+ consent has a **flow record**, `oauth:flow:<sha256(state)>`, holding its epoch
1374
+ and timestamp, its binding to the selected server and client, its consent URL,
1375
+ and its PKCE verifier for fifteen minutes; the state itself is stored nowhere
1376
+ outside that URL.
1370
1377
 
1371
- With a vault, the grant's body and each verifier are sealed with its AES-GCM
1372
- key, the additional authenticated data naming the connector, owner partition,
1378
+ With a vault, the grant's body and each flow record's binding, consent URL and
1379
+ verifier are sealed with its AES-GCM key, the additional authenticated data naming the connector, owner partition,
1373
1380
  record, and epoch, so ciphertext moved to another connector, principal,
1374
1381
  record, or epoch does not open. Anything that fails to open (tampered, or
1375
1382
  under a rotated key) reads as absent: `auth_required` and a logged warning.
1376
- The epoch, consent URL, and state digest stay plaintext and authenticate
1377
- nothing. A plaintext body written before the vault could seal is sealed where
1383
+ The epoch, timestamp, and state digest stay plaintext and authenticate nothing. A plaintext body written before the vault could seal is sealed where
1378
1384
  it lies on first read, by compare-and-set. An [`api()` OAuth
1379
- connector](#downstream-oauth-on-api) stores no client. A vault without the
1380
- optional `seal`/`open` members keeps the body plaintext with a startup
1381
- warning; no vault, plaintext as always.
1385
+ connector](#downstream-oauth-on-api) stores no client.
1386
+
1387
+ `createConnecta` throws when a configured vault lacks `seal` or `open` and any
1388
+ connector has `startAuth`. A vault without them still works for deployments with
1389
+ no OAuth connector. To migrate, use `encryptedCredentialVault(storage, key)` or
1390
+ add both methods; grants stored plaintext are then sealed on first read as above.
1391
+
1392
+ **With no vault, the guarantee is narrower.** Nothing is encrypted at rest: the
1393
+ grant's body and the PKCE verifier are plaintext, and a configured URL's path can
1394
+ appear in them. The flow record is minimized instead. It stores no consent URL,
1395
+ and its binding keeps no URL userinfo, query or fragment, in plaintext or in its
1396
+ unkeyed discovery digest. Connect, callback and restart still work, and two things
1397
+ differ. A repeated non-forced start mints a fresh consent rather than handing the
1398
+ pending URL back; earlier links stay valid until their epoch ends. A change confined
1399
+ to a URL's query or fragment does not fence a pending consent.
1400
+ Use a vault wherever stored OAuth state must be protected.
1382
1401
 
1383
1402
  The body also holds `expiresAt`, the epoch milliseconds the stored access token
1384
1403
  expires: its `expires_in`, counted from just before the request that issued it
@@ -1423,7 +1442,7 @@ longer live; one left behind cannot complete and expires with its link.
1423
1442
  `authorize_connector` issues the same browser link, with `force` carried in the
1424
1443
  signed handoff. On hosts without URL elicitation, direct and program auth
1425
1444
  failures and search/describe catalog failures include the same authorization
1426
- handoff. The admitted registry and management permission govern issuance.
1445
+ handoff. A declined or cancelled URL elicitation returns it too. The admitted registry and management permission govern issuance.
1427
1446
  Each call or program run shares at most one handoff per connector; errors never
1428
1447
  request a forced restart. Signed links retain their fifteen-minute expiry,
1429
1448
  initiating-user and owner binding, origin check, and single-use browser nonce.
@@ -1662,7 +1681,7 @@ If the refresh fails instead, whatever it met (a dead grant, a waiter
1662
1681
  deadline, the caller leaving), or hands back the token the API just
1663
1682
  rejected, the 401's verdict stays:
1664
1683
  `downstream_oauth_required`, never retryable, with `reconciliationRequired`
1665
- and `writeOutcome: "unknown"`, and an `authorize_connector` handoff that says
1684
+ and `writeOutcome: "unknown"`, and a handoff that says
1666
1685
  to re-read the target before any fresh call. A read is refreshed and sent
1667
1686
  once more. MCP sends a tool only once when either its effective classification
1668
1687
  or its original server hints say write. A deployment write-to-read override
@@ -1993,9 +2012,14 @@ any registered form is still withheld, however short.
1993
2012
  Identifiers sent beside a credential are matched whole only, because consent
1994
2013
  URLs and vendor metadata carry their parts by design: a confidential OAuth
1995
2014
  client's ID, a Basic username beside a password of at least eight characters,
1996
- and credential-slot fields named as identifiers (ending in `id`, `user`,
1997
- `username`, `login`, or `email`, or starting with `public`). A Basic username
1998
- beside a shorter or empty password is the credential itself and is fully
2015
+ credential-slot fields named as identifiers (ending in `id`, `user`,
2016
+ `username`, `login`, or `email`, or starting with `public`), and the two headers
2017
+ maintained providers declare public from their vendors' contracts: Cloudflare's
2018
+ Global API Key `X-Auth-Email` and Overflow's `x-client-id`. A value in either
2019
+ that holds whitespace, as an auth scheme's framing does, is a credential. Any
2020
+ other header or query parameter is never public by name: `remoteMcp()` and
2021
+ `api()` auth headers under any name are credentials, fully matched. A Basic
2022
+ username beside a shorter or empty password is the credential itself and is fully
1999
2023
  matched. Query parameters named as continuation cursors (`pageToken`,
2000
2024
  `nextPageToken`, `next_token`, `continuationToken`, `syncToken`) are public
2001
2025
  result state and are not registered. Header values a connector adds to token
@@ -2083,11 +2107,17 @@ targeting the host's private network:
2083
2107
  - A URL on the connector's configured origin is trusted; the operator wrote it.
2084
2108
  - Any other URL must be `https` and must not name a private host: `localhost`
2085
2109
  and `*.localhost`, `127/8`, `10/8`, `172.16/12`, `192.168/16`, `169.254/16`,
2086
- `100.64/10`, `0/8`, `::`, `::1`, `fc00::/7`, `fe80::/10`, or an IPv4-mapped
2087
- IPv6 form of any of those.
2110
+ `100.64/10`, `0/8`, `192.0.0/24`, `192.0.2/24`, `192.88.99/24`, `198.18/15`,
2111
+ `198.51.100/24`, `203.0.113/24`, `224/4`, `240/4`, `::`, `::1`, any IPv6
2112
+ address outside global unicast `2000::/3` (which covers `fc00::/7`,
2113
+ `fe80::/10`, `fec0::/10`, `ff00::/8` and `64:ff9b:1::/48`), `2001::/23`,
2114
+ `2001:db8::/32`, or `3fff::/20`. An IPv6 address carrying an IPv4 address in
2115
+ mapped, compatible, translated, NAT64 (`64:ff9b::/96`) or 6to4 (`2002::/16`)
2116
+ form is judged by the address it carries.
2088
2117
  - A connector configured on a loopback host may also learn loopback URLs, over
2089
2118
  `http` too, so a local MCP server with a local authorization server keeps
2090
- working. LAN and link-local addresses stay refused.
2119
+ working. Of the IPv4-carrying forms, only IPv4-mapped loopback counts as
2120
+ loopback. LAN and link-local addresses stay refused.
2091
2121
 
2092
2122
  A refused URL is never requested. Discovery, registration, and code exchange
2093
2123
  fail with a non-retryable `connector_call_failed`. Resource-metadata origin
@@ -2098,10 +2128,22 @@ and the grant requires re-consent. Redirects cannot route around the
2098
2128
  rule; token requests never follow them, and resource redirects follow only
2099
2129
  same-origin hops.
2100
2130
 
2101
- The check is syntactic: it reads the host after the WHATWG URL parser folds
2102
- `2130706433` and `0x7f.1` into `127.0.0.1`, and never resolves a name — the
2103
- Workers-safe core has no DNS. A public name resolving or rebinding to a private
2104
- address is out of scope; a host that must stop that needs an egress policy.
2131
+ The shared check reads canonical URL syntax (including legacy IPv4 spellings).
2132
+ On Node, the owned transport also resolves learned hosts immediately before
2133
+ connecting, refuses the whole answer set if any IPv4 or IPv6 address violates
2134
+ the same rules, and pins the connection to a checked address. TLS SNI and
2135
+ certificate validation retain the original hostname, including through a proxy.
2136
+ Proxy TLS authenticates the proxy hostname separately from destination TLS.
2137
+ Resolution uses request-owned A/AAAA queries, cancelled when the request ends
2138
+ and bounded to ten seconds even without a caller deadline. Only absent address
2139
+ families may be omitted; resolver failures refuse the request. These DNS queries
2140
+ do not consult OS hosts-file aliases.
2141
+ Each redirect hop is checked. Configured origins remain exempt. Resolution
2142
+ refusals are typed, non-retryable, and send no destination request.
2143
+
2144
+ Workers exposes no connection-time DNS hook and has no route to private networks;
2145
+ its behavior remains the shared syntactic check. The shared core never imports
2146
+ Node networking or the optional undici peer.
2105
2147
 
2106
2148
  Downstream advertisements choose consent destinations, never destinations for
2107
2149
  existing grants. Tokens and registered clients bind to their issuing server;
@@ -2228,7 +2270,9 @@ the first request on a fresh instance. It grants no identity and never falls
2228
2270
  back to another provider. Missing context without an assertion still returns 401. An unusable trusted AUD returns retryable 503 with
2229
2271
  `problem: "access_audience_invalid"`; it never proceeds to identity lookup.
2230
2272
  A rejected human identity, including a service identity or an unusable user
2231
- id, returns 403. A thrown identity lookup or its five-second deadline returns retryable
2273
+ id, returns 403. A thrown identity lookup gets one retry after 100–250 ms of
2274
+ jitter, within the original five-second deadline. A hanging lookup is never
2275
+ retried concurrently. The second rejection or the deadline returns retryable
2232
2276
  503 with `problem: "identity_unavailable"` and `Retry-After: 1`. Every refusal
2233
2277
  carries `Cache-Control: no-store`; every unavailable-verification 503 carries
2234
2278
  `Retry-After: 1` and no `WWW-Authenticate` challenge. None of these failures admits an identity,
@@ -2246,9 +2290,24 @@ identifies the generic route fallthrough, `authorization_header_invalid` a
2246
2290
  malformed header, and `authentication_failed` an unclassified provider refusal.
2247
2291
  Access verification 503s emit `[connecta] Cloudflare Access verification unavailable`
2248
2292
  with the same facts and `access_context_unavailable`, `access_audience_invalid`,
2249
- or `access_identity_unavailable`. These records contain no header values,
2293
+ or `access_identity_unavailable`. Identity lookup failures additionally carry
2294
+ checked `identityLookup` facts: `outcome` (`timeout` or `rejected`), `durationMs`,
2295
+ `cold` (whether this was the isolate's first identity lookup), and `inFlight`
2296
+ (the isolate's lookup attempts still pending when the failure was recorded; this
2297
+ request's own attempt counts only while it is pending, so a hung lookup counts
2298
+ itself, and a rejected lookup or a deadline during retry backoff does not).
2299
+ These records contain no header values,
2250
2300
  assertions, cookies, identities, URLs, response bodies, or exception text.
2251
2301
 
2302
+ Identity lookups stay request-owned: Connecta neither coalesces them across
2303
+ requests nor caches identity results. A caller-supplied assertion header is
2304
+ not an identity cache key. Failed identity lookups always refuse admission.
2305
+
2306
+ After an authorized release, validate this mitigation with about 20 concurrent
2307
+ sessions. Count runs containing `identity_unavailable`, targeting fewer than
2308
+ 1% of runs; distinguish `timeout` from `rejected` in the checked lookup facts.
2309
+ A passing local concurrency fixture does not establish production acceptance.
2310
+
2252
2311
  Protect the Worker with a Worker-level Access application whose destination is
2253
2312
  `{ "type": "worker", "worker_id": "<the Worker script tag>" }`. A traditional
2254
2313
  hostname-level application blocks the URL but does not attach `ctx.access` to
@@ -2293,7 +2352,15 @@ tokens by default, both JWT and opaque. Verified `aud` or `resource` must
2293
2352
  contain the endpoint's exact canonical URL from protected-resource metadata.
2294
2353
  If both claims are present, both must match. Trailing slashes, case, queries,
2295
2354
  and fragments are not normalized. `/mcp` and `/mcp/support` are distinct
2296
- resources. Set `publicUrl` behind an internal origin.
2355
+ resources.
2356
+
2357
+ Clerk needs a fixed deployment URL. Set `ConnectaConfig.publicUrl`, or
2358
+ `clerkAuth({ publicUrl })` when only Clerk should use it; `createConnecta` throws
2359
+ when neither is set, on Node and on Workers. Both token `resource` checks and the
2360
+ session `azp` pin read this value, so a request's `Host` and `X-Forwarded-Proto`
2361
+ never decide them. There is no implicit loopback default: a local deployment names
2362
+ its origin, such as `http://localhost:8787`. The Node template does this when
2363
+ `PUBLIC_URL` is unset.
2297
2364
 
2298
2365
  ```ts
2299
2366
  clerkAuth({ publishableKey, secretKey,
@@ -2548,6 +2615,30 @@ survives every round. Rotation invalidates outstanding states.
2548
2615
  Each state admits one retry through a connector-scoped storage CAS, including
2549
2616
  decline and cancel. Concurrent or repeated consumption is refused before
2550
2617
  dispatch, so retries cannot fork into sibling forced connection links.
2618
+ A decline or cancel returns one fresh link, issued only after that state verifies
2619
+ and the same management check as the prompt passes, so a refusal offers nothing
2620
+ this caller could not already request. The link is sealed like the prompt's and
2621
+ keeps the prompt's force.
2622
+
2623
+ URL `accept` is consent to the interaction, not evidence that a browser navigated:
2624
+ the retry normally reaches Connecta before any visit does. So every link a flow
2625
+ issues stays live until it is visited or expires, whichever round or refusal
2626
+ issued it. A `force: true` flow resets once, through one atomic claim on an identity
2627
+ its links share:
2628
+
2629
+ | A verified visit finds the reset | The visit |
2630
+ | -------------------------------- | ------------------------------------------------------------------------------- |
2631
+ | Unclaimed | Claims it and runs `startAuth({ force: true })` |
2632
+ | Still running in another visit | Returns 409 and spends nothing, so reloading the same link retries |
2633
+ | Started | Spends its link and runs `startAuth({ force: false })`, continuing that consent |
2634
+ | Failed | Claims it again and starts afresh, so a failure never reuses the old grant |
2635
+
2636
+ A failed link is spent, but the flow is not: its other links, or the next prompt's,
2637
+ take over. The record outlives every link the flow can issue. If the process holding
2638
+ the claim dies, nothing settles it, so the flow's links keep returning 409 for up to
2639
+ thirty minutes, and that response says to start a new connection request. A new
2640
+ `force: true` call is a new flow with its own reset, so it recovers at once. Each link still works
2641
+ once. An unforced flow's links stay independent and never reset.
2551
2642
  The host records each entered call's classification, catalog freshness and
2552
2643
  classification digest in private invocation state, along with auth failure origins. Writes may elicit only
2553
2644
  when Connecta's own credential resolution fails before invoking the connector:
@@ -2582,14 +2673,11 @@ reads this key; connector contexts and guest APIs cannot reach the vault.
2582
2673
  Explicit authorization retries require a completed browser start (including
2583
2674
  its reset and consent binding), valid stored credential fields where declared,
2584
2675
  and a healthy passive connector status where available before completing.
2585
- An in-progress start cannot complete against an old grant. A forced flow switches to
2586
- Continue after that visit, so another prompt cannot restart pending consent.
2587
- Failed starts remain single-use but do not block a later successful attempt.
2588
- Before checking completion, unspent links in the retry flow are atomically
2589
- retired so an older forced link cannot begin a reset during the status check.
2590
- A Continue link issued during a pending forced start is bound to that
2591
- predecessor and cannot be consumed unless the predecessor starts successfully.
2592
- If it fails, the next prompt requests a fresh restart instead of reusing the old grant.
2676
+ An in-progress start cannot complete against an old grant: only a link whose start
2677
+ finished counts toward completion, and the next prompt carries the same flow, so it
2678
+ cannot restart pending consent. Failed starts remain single-use but do not block a later
2679
+ successful attempt. A link visited during the completion check continues the
2680
+ attempt that check is judging, and cannot reset it.
2593
2681
 
2594
2682
  The `/connect` link issued by the elicitation boundary encrypts its browser
2595
2683
  handoff with the vault's `seal`/`open` methods before signing it. The URL carries
@@ -2658,12 +2746,13 @@ still serve configured credentials. Both routes accept only GET, otherwise retur
2658
2746
  `form_post` is unsupported. Clerk's session-only policy applies regardless of method.
2659
2747
 
2660
2748
  The fifteen-minute signed handoff carries connector ownership, initiating principal, deployment origin, nonce,
2661
- restart mode, and any predecessor link binding. New links use versioned, length-prefixed fields instead of
2749
+ restart mode, and, for a forced flow's later links, the flow they share a reset with. New links use versioned, length-prefixed fields instead of
2662
2750
  JSON, with unpadded base64url payload and signature segments. Ordinary handoffs use an opaque wire encoding,
2663
2751
  not encryption; URL elicitation also seals those fields as described above. Copy the complete URL unchanged.
2664
2752
  The signature authenticates the exact version and payload text. Tampered or re-encoded links return
2665
2753
  `Invalid or expired connection link. Request a new link from connecta.` Pre-upgrade signed JSON and sealed
2666
- v2 links still verify against their original text and expire after their original fifteen minutes.
2754
+ v2 links still verify against their original text and expire after their original fifteen minutes. A link from
2755
+ an earlier version that names a predecessor link, in any format, returns the same refusal, and a new link replaces it.
2667
2756
 
2668
2757
  Configure `vault: encryptedCredentialVault(storage, key)`, which derives its HMAC key with HKDF. Custom vaults need `signOAuthHandoff` and `verifyOAuthHandoff`; without a signing vault, links cannot be
2669
2758
  issued or accepted. A link grants no identity. After identity and permissions pass, its nonce is consumed
@@ -2900,12 +2989,12 @@ owner's access token as `Authorization: Bearer` with these rules:
2900
2989
  - Whatever that recovery meets, a write's failure is never retryable and its
2901
2990
  outcome is unknown. Until a refresh succeeds the 401's verdict stands:
2902
2991
  `downstream_oauth_required`, which the invocation marks
2903
- `reconciliationRequired`, with the `authorize_connector` handoff, whether
2992
+ `reconciliationRequired`, with the authorization handoff, whether
2904
2993
  the refresh was [dead](#refresh-failures), lost after it was sent, raced
2905
2994
  by a disconnect or reset, failed in storage or contention, or handed back
2906
2995
  the token the API just rejected, which renews nothing.
2907
2996
  - No grant, a second 401, or a [dead refresh](#refresh-failures) means
2908
- `downstream_oauth_required`, directing `authorize_connector`. Refresh waiter deadlines
2997
+ `downstream_oauth_required`, carrying the authorization handoff. Refresh waiter deadlines
2909
2998
  return retryable `unavailable` for a read. Latch second 401s for the request scope,
2910
2999
  preventing repeated refreshes by later program calls.
2911
3000
  - Handlers can name no storage, sealing, or owner partition; the registry owns them.
@@ -2923,9 +3012,27 @@ grant with different consent settings.
2923
3012
 
2924
3013
  The grant record and consents, epoch fencing, vault sealing, shared/personal ownership,
2925
3014
  callback state/principal checks, `authorize_connector`, Connect, Restart, and
2926
- Disconnect match `remoteMcp()`. Status calls report a stored grant for the
2927
- configured token endpoint healthy without downstream probes and never start
2928
- authorization. `oauth` and
3015
+ Disconnect match `remoteMcp()`. Status reads do not probe tokens, start OAuth,
3016
+ or write grant state. A grant for the configured token endpoint starts as `ok`;
3017
+ a real API 401 records `refused` and status becomes `auth_required`. A real API
3018
+ 2xx records `accepted`. Other HTTP statuses do not establish an auth outcome.
3019
+ A persisted refusal does not block a later invocation: the API may have recovered,
3020
+ and an accepted response restores `ok`. Within the failing request, a second 401
3021
+ still latches authorization failure. Connect on a refused grant begins fresh consent
3022
+ rather than reporting existing tokens as healthy. Maintained OAuth providers record
3023
+ these same outcomes for initial and replayed requests. Repeated identical outcomes
3024
+ for the current revision do not rewrite the grant.
3025
+ Each consent and refresh advances a monotonic token revision within its grant
3026
+ epoch and clears the prior observation in the same compare-and-set write.
3027
+ Call observations carry the epoch and revision read with their token and are
3028
+ accepted only while both still match the live grant. No wall clock orders
3029
+ observations. Reconsent with an identical token still advances the revision.
3030
+ The observation stores only its revision and the `accepted`/`refused` enum;
3031
+ arguments, response bodies, code and error text are excluded. A declared oversized
3032
+ 401 records refusal without reading its body, including on a replay. Normal Connect
3033
+ resets only while the refused epoch, revision and outcome it read still match;
3034
+ a concurrently recovered grant is preserved. An observation
3035
+ storage failure does not replace the already received API response. `oauth` and
2929
3036
  `credential` are exclusive per connector, giving `auth_required` one recovery.
2930
3037
  For providers offering both, deployment config chooses; handlers check `ctx.oauth`.
2931
3038