@zackbart/connecta 0.28.1 → 0.30.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 (332) hide show
  1. package/CHANGELOG.md +855 -5
  2. package/README.md +125 -116
  3. package/bin/connecta.mjs +121 -139
  4. package/bin/meta-tool-names.d.mts +2 -0
  5. package/bin/meta-tool-names.mjs +9 -0
  6. package/bin/version.mjs +3 -0
  7. package/dist/access-tokens.js +53 -63
  8. package/dist/activity-request.js +8 -0
  9. package/dist/activity.d.ts +76 -42
  10. package/dist/activity.js +110 -42
  11. package/dist/argument-redaction.js +230 -0
  12. package/dist/auth/clerk-sdk/client.d.ts +65 -0
  13. package/dist/auth/clerk-sdk/client.js +5 -0
  14. package/dist/auth/clerk-sdk/generated.js +1845 -0
  15. package/dist/auth/clerk-transport.js +19 -0
  16. package/dist/auth/clerk.d.ts +17 -7
  17. package/dist/auth/clerk.js +189 -107
  18. package/dist/auth/cloudflare-access.js +25 -26
  19. package/dist/auth/downstream-client-metadata.d.ts +29 -0
  20. package/dist/auth/downstream-client-metadata.js +133 -0
  21. package/dist/auth/downstream-oauth.js +1584 -1580
  22. package/dist/auth/downstream-revocation.js +106 -0
  23. package/dist/auth/oauth-token-response.js +231 -0
  24. package/dist/auth/oauth-v2-migration.js +116 -0
  25. package/dist/auth/static-oauth.js +93 -71
  26. package/dist/auth-elicitation.js +288 -0
  27. package/dist/authorization-handoff.js +111 -0
  28. package/dist/branding.d.ts +8 -16
  29. package/dist/branding.js +26 -59
  30. package/dist/byte-read-response.js +39 -0
  31. package/dist/call-admission.js +12 -16
  32. package/dist/catalog-cache.js +739 -0
  33. package/dist/catalog-drift.d.ts +50 -15
  34. package/dist/catalog-drift.js +365 -128
  35. package/dist/catalog-fingerprint.js +2 -4
  36. package/dist/catalog-freshness.js +13 -0
  37. package/dist/catalog-limits.js +2 -2
  38. package/dist/catalog-service.js +409 -225
  39. package/dist/catalog.js +43 -84
  40. package/dist/concurrency.js +3 -4
  41. package/dist/config-defaults.d.ts +39 -0
  42. package/dist/config-defaults.js +46 -0
  43. package/dist/config-schema.d.ts +181 -0
  44. package/dist/config-schema.js +435 -0
  45. package/dist/config-value-sources.js +102 -0
  46. package/dist/config.d.ts +633 -0
  47. package/dist/config.js +508 -0
  48. package/dist/connector-access.d.ts +1 -0
  49. package/dist/connector-access.js +13 -15
  50. package/dist/connector-caller.d.ts +8 -0
  51. package/dist/connector-caller.js +15 -6
  52. package/dist/connector-scope.js +1 -1
  53. package/dist/connectors/api-connector.d.ts +59 -7
  54. package/dist/connectors/api-connector.js +106 -19
  55. package/dist/connectors/api.js +4 -1
  56. package/dist/connectors/guarded-fetch.d.ts +144 -0
  57. package/dist/connectors/guarded-fetch.js +87 -38
  58. package/dist/connectors/negotiation-cache.js +46 -0
  59. package/dist/connectors/option-shapes.js +27 -0
  60. package/dist/connectors/remote-mcp.d.ts +30 -4
  61. package/dist/connectors/remote-mcp.js +1774 -320
  62. package/dist/connectors/resource-uri.js +335 -0
  63. package/dist/credential-contract.d.ts +10 -0
  64. package/dist/credential-rules.js +1 -3
  65. package/dist/credential-url.js +108 -0
  66. package/dist/credentials.d.ts +6 -1
  67. package/dist/credentials.js +27 -18
  68. package/dist/d1.d.ts +130 -0
  69. package/dist/d1.js +290 -0
  70. package/dist/describe-config.d.ts +165 -0
  71. package/dist/describe-config.js +315 -0
  72. package/dist/described.js +106 -0
  73. package/dist/downstream-input-context.js +128 -0
  74. package/dist/downstream-input.js +325 -0
  75. package/dist/errors.d.ts +132 -31
  76. package/dist/errors.js +175 -87
  77. package/dist/execute.js +798 -318
  78. package/dist/executor-admission.d.ts +1 -0
  79. package/dist/executor-admission.js +11 -15
  80. package/dist/executor-contract.d.ts +20 -0
  81. package/dist/executor-contract.js +48 -0
  82. package/dist/executor-result.js +30 -13
  83. package/dist/executors/quickjs-child.js +15 -9
  84. package/dist/executors/quickjs-protocol.js +5 -6
  85. package/dist/executors/quickjs-runtime.js +152 -59
  86. package/dist/executors/quickjs.js +49 -39
  87. package/dist/favicon.js +1 -2
  88. package/dist/fix-prompt.js +1 -3
  89. package/dist/guest-runtime.js +238 -0
  90. package/dist/guest-types.d.ts +218 -0
  91. package/dist/guest-types.js +1 -0
  92. package/dist/html-security.js +24 -0
  93. package/dist/identity.js +1 -3
  94. package/dist/inbound-credential.js +52 -0
  95. package/dist/index.d.ts +24 -256
  96. package/dist/index.js +65 -478
  97. package/dist/invocation-auth.js +69 -0
  98. package/dist/invocation.js +262 -79
  99. package/dist/json-schema.d.ts +1 -1
  100. package/dist/mcp-client-context.js +35 -0
  101. package/dist/mcp-result.js +11 -7
  102. package/dist/meta-output.js +61 -0
  103. package/dist/meta-tool-names.d.ts +2 -0
  104. package/dist/meta-tool-names.js +3 -0
  105. package/dist/meta-tools.js +419 -272
  106. package/dist/module-contracts.d.ts +8 -23
  107. package/dist/node.d.ts +0 -2
  108. package/dist/node.js +2 -2
  109. package/dist/oauth-callback-outcome.js +1 -3
  110. package/dist/oauth-handoff.js +194 -0
  111. package/dist/oauth-sealing.js +20 -0
  112. package/dist/operator-record.d.ts +137 -0
  113. package/dist/operator-record.js +420 -0
  114. package/dist/operator-ui/assets.js +31 -0
  115. package/dist/operator-ui/contract.d.ts +72 -0
  116. package/dist/operator-ui/contract.js +1 -0
  117. package/dist/operator-ui/fix-prompts.d.ts +18 -0
  118. package/dist/operator-ui/generated.js +5 -4
  119. package/dist/operator-ui/model.d.ts +12 -87
  120. package/dist/operator-ui/model.js +2 -9
  121. package/dist/operator-ui/setup-commands.js +1 -3
  122. package/dist/operator-ui/view.d.ts +323 -0
  123. package/dist/operator-ui/view.js +62 -189
  124. package/dist/page-styles.js +2 -3
  125. package/dist/program-result.js +5 -0
  126. package/dist/program-source.js +14 -2
  127. package/dist/{exempt-writes.js → program-writes.js} +60 -19
  128. package/dist/provider.d.ts +142 -0
  129. package/dist/provider.js +187 -0
  130. package/dist/providers/_shared/google/result-size.js +57 -0
  131. package/dist/providers/_shared/google/service-account.d.ts +58 -0
  132. package/dist/providers/_shared/google/service-account.js +500 -0
  133. package/dist/providers/_shared/google/workspace.d.ts +276 -0
  134. package/dist/providers/_shared/google/workspace.js +518 -0
  135. package/dist/providers/_shared/request-token.js +83 -0
  136. package/dist/providers/{basecamp.d.ts → basecamp/index.d.ts} +6 -28
  137. package/dist/providers/basecamp/index.js +997 -0
  138. package/dist/providers/basecamp/skill.generated.js +9 -0
  139. package/dist/providers/{breeze.d.ts → breeze/index.d.ts} +2 -2
  140. package/dist/providers/{breeze.js → breeze/index.js} +184 -140
  141. package/dist/providers/breeze/skill.generated.js +9 -0
  142. package/dist/providers/{ccb.d.ts → ccb/index.d.ts} +2 -2
  143. package/dist/providers/{ccb.js → ccb/index.js} +271 -116
  144. package/dist/providers/ccb/skill.generated.js +9 -0
  145. package/dist/providers/{cloudflare.d.ts → cloudflare/index.d.ts} +8 -7
  146. package/dist/providers/{cloudflare.js → cloudflare/index.js} +171 -738
  147. package/dist/providers/cloudflare/skill.generated.js +10 -0
  148. package/dist/providers/docs/index.d.ts +54 -0
  149. package/dist/providers/docs/index.js +1237 -0
  150. package/dist/providers/docs/skill.generated.js +9 -0
  151. package/dist/providers/drive/index.d.ts +53 -0
  152. package/dist/providers/drive/index.js +1523 -0
  153. package/dist/providers/drive/skill.generated.js +9 -0
  154. package/dist/providers/forms/index.d.ts +54 -0
  155. package/dist/providers/forms/index.js +1217 -0
  156. package/dist/providers/forms/skill.generated.js +11 -0
  157. package/dist/providers/github/auth.d.ts +32 -0
  158. package/dist/providers/github/auth.js +361 -0
  159. package/dist/providers/github/hosted.js +218 -0
  160. package/dist/providers/github/index.d.ts +13 -0
  161. package/dist/providers/github/index.js +140 -0
  162. package/dist/providers/github/key.js +100 -0
  163. package/dist/providers/github/rest.js +321 -0
  164. package/dist/providers/github/scope.d.ts +29 -0
  165. package/dist/providers/github/scope.js +124 -0
  166. package/dist/providers/github/skill.generated.js +8 -0
  167. package/dist/providers/gmail/index.d.ts +51 -0
  168. package/dist/providers/gmail/index.js +1436 -0
  169. package/dist/providers/gmail/skill.generated.js +9 -0
  170. package/dist/providers/infisical/index.d.ts +7 -0
  171. package/dist/providers/infisical/index.js +713 -0
  172. package/dist/providers/infisical/skill.generated.js +8 -0
  173. package/dist/providers/{linear.d.ts → linear/index.d.ts} +11 -14
  174. package/dist/providers/linear/index.js +352 -0
  175. package/dist/providers/linear/skill.generated.js +8 -0
  176. package/dist/providers/{mixpanel.d.ts → mixpanel/index.d.ts} +5 -10
  177. package/dist/providers/mixpanel/index.js +416 -0
  178. package/dist/providers/mixpanel/skill.generated.js +9 -0
  179. package/dist/providers/{notion.d.ts → notion/index.d.ts} +8 -8
  180. package/dist/providers/{notion.js → notion/index.js} +226 -239
  181. package/dist/providers/notion/skill.generated.js +10 -0
  182. package/dist/providers/{overflow.d.ts → overflow/index.d.ts} +2 -2
  183. package/dist/providers/{overflow.js → overflow/index.js} +74 -111
  184. package/dist/providers/overflow/skill.generated.js +8 -0
  185. package/dist/providers/{planning-center.d.ts → planning-center/index.d.ts} +2 -2
  186. package/dist/providers/{planning-center.js → planning-center/index.js} +264 -104
  187. package/dist/providers/planning-center/skill.generated.js +10 -0
  188. package/dist/providers/{revenuecat.d.ts → revenuecat/index.d.ts} +5 -10
  189. package/dist/providers/revenuecat/index.js +557 -0
  190. package/dist/providers/revenuecat/skill.generated.js +11 -0
  191. package/dist/providers/sheets/index.d.ts +54 -0
  192. package/dist/providers/sheets/index.js +1237 -0
  193. package/dist/providers/sheets/skill.generated.js +10 -0
  194. package/dist/providers/slides/index.d.ts +53 -0
  195. package/dist/providers/slides/index.js +2645 -0
  196. package/dist/providers/slides/skill.generated.js +9 -0
  197. package/dist/providers/{stripe.d.ts → stripe/index.d.ts} +5 -12
  198. package/dist/providers/stripe/index.js +266 -0
  199. package/dist/providers/stripe/skill.generated.js +11 -0
  200. package/dist/providers/{tithely.d.ts → tithely/index.d.ts} +2 -2
  201. package/dist/providers/{tithely.js → tithely/index.js} +60 -91
  202. package/dist/providers/tithely/skill.generated.js +8 -0
  203. package/dist/providers/{vercel.d.ts → vercel/index.d.ts} +8 -8
  204. package/dist/providers/vercel/index.js +1038 -0
  205. package/dist/providers/vercel/mcp-ownership.js +212 -0
  206. package/dist/providers/vercel/skill.generated.js +11 -0
  207. package/dist/registry.d.ts +110 -171
  208. package/dist/registry.js +588 -927
  209. package/dist/request-state.js +21 -0
  210. package/dist/resource-read.js +103 -0
  211. package/dist/result-shapes.js +17 -49
  212. package/dist/retired-toolkits.js +1 -1
  213. package/dist/routes/access-tokens.js +3 -6
  214. package/dist/routes/activity-context.js +21 -0
  215. package/dist/routes/activity-disclosure.js +40 -0
  216. package/dist/routes/activity.js +59 -21
  217. package/dist/routes/actor-labels.js +3 -50
  218. package/dist/routes/connect.js +221 -0
  219. package/dist/routes/credentials.js +15 -27
  220. package/dist/routes/mcp.js +365 -130
  221. package/dist/routes/oauth-client-metadata.js +15 -0
  222. package/dist/routes/oauth-management.js +45 -107
  223. package/dist/routes/oauth.js +110 -57
  224. package/dist/routes/operator.js +3 -3
  225. package/dist/routes/shared.d.ts +25 -64
  226. package/dist/routes/shared.js +160 -80
  227. package/dist/routes/skills.js +30 -0
  228. package/dist/routes/ui-config.js +225 -0
  229. package/dist/routes/ui-data.js +40 -35
  230. package/dist/routes/ui.js +66 -56
  231. package/dist/runtime/admission.js +4 -2
  232. package/dist/runtime/call-admission.js +3 -1
  233. package/dist/runtime/connector-scope.js +1 -4
  234. package/dist/runtime/run.js +10 -39
  235. package/dist/runtime/services.js +12 -117
  236. package/dist/runtime/shared-read.js +1 -3
  237. package/dist/runtime/storage.js +8 -1
  238. package/dist/sent-secrets.d.ts +93 -0
  239. package/dist/sent-secrets.js +1013 -0
  240. package/dist/server.js +28 -71
  241. package/dist/skills.js +416 -174
  242. package/dist/sqlite.d.ts +48 -0
  243. package/dist/sqlite.js +181 -0
  244. package/dist/storage/keys.js +443 -0
  245. package/dist/storage/memory.js +16 -4
  246. package/dist/storage/sql.d.ts +66 -0
  247. package/dist/storage/sql.js +543 -0
  248. package/dist/timeout.js +4 -2
  249. package/dist/tool-name.js +17 -0
  250. package/dist/tool-safety.d.ts +9 -35
  251. package/dist/tool-safety.js +20 -40
  252. package/dist/types.d.ts +336 -56
  253. package/dist/types.js +0 -1
  254. package/dist/ui.d.ts +7 -19
  255. package/dist/ui.js +76 -90
  256. package/dist/url-safety.js +3 -6
  257. package/dist/usage-guide.js +146 -0
  258. package/dist/validate.d.ts +7 -0
  259. package/dist/validate.js +309 -47
  260. package/dist/version.d.ts +1 -7
  261. package/dist/version.js +3 -7
  262. package/dist/worker.d.ts +11 -0
  263. package/dist/worker.js +435 -0
  264. package/documentation/README.md +63 -0
  265. package/documentation/architecture.md +696 -231
  266. package/documentation/auth.md +1069 -546
  267. package/documentation/code-mode.md +662 -356
  268. package/documentation/deploying.md +146 -0
  269. package/documentation/integrating.md +159 -0
  270. package/documentation/meta-tools.md +570 -216
  271. package/documentation/operating.md +135 -0
  272. package/documentation/operator-tests.md +97 -0
  273. package/documentation/operator-ui.md +132 -0
  274. package/documentation/provider-migration-0.29.md +65 -0
  275. package/examples/worker/AGENTS.md +34 -12
  276. package/examples/worker/README.md +293 -255
  277. package/examples/worker/kv-to-d1.wrangler.jsonc +26 -0
  278. package/examples/worker/scripts/copy-kv-to-d1.mjs +162 -0
  279. package/examples/worker/src/connecta.config.ts +151 -0
  280. package/examples/worker/src/index.ts +14 -197
  281. package/examples/worker/wrangler.jsonc +15 -35
  282. package/package.json +126 -54
  283. package/templates/node/.dockerignore +1 -2
  284. package/templates/node/.env.example +20 -21
  285. package/templates/node/AGENTS.md +21 -17
  286. package/templates/node/Dockerfile +3 -5
  287. package/templates/node/README.md +140 -58
  288. package/templates/node/docker-compose.yml +11 -12
  289. package/templates/node/package.json +4 -2
  290. package/templates/node/src/connecta.config.ts +119 -0
  291. package/templates/node/src/index.ts +9 -154
  292. package/templates/node/src/provision-token.ts +17 -0
  293. package/AGENTS.md +0 -170
  294. package/dist/artifacts/connector.d.ts +0 -58
  295. package/dist/artifacts/connector.js +0 -730
  296. package/dist/artifacts/document.js +0 -86
  297. package/dist/artifacts/guide.js +0 -140
  298. package/dist/artifacts/html-scan.js +0 -329
  299. package/dist/artifacts/json.js +0 -78
  300. package/dist/artifacts/kv-store.d.ts +0 -20
  301. package/dist/artifacts/kv-store.js +0 -140
  302. package/dist/artifacts/markdown.js +0 -478
  303. package/dist/artifacts/module.d.ts +0 -37
  304. package/dist/artifacts/module.js +0 -66
  305. package/dist/artifacts/operations.d.ts +0 -197
  306. package/dist/artifacts/operations.js +0 -1002
  307. package/dist/artifacts/refresh.d.ts +0 -55
  308. package/dist/artifacts/refresh.js +0 -236
  309. package/dist/artifacts/routes.js +0 -206
  310. package/dist/artifacts/types.d.ts +0 -214
  311. package/dist/artifacts/types.js +0 -22
  312. package/dist/artifacts/validate.d.ts +0 -62
  313. package/dist/artifacts/validate.js +0 -651
  314. package/dist/artifacts.d.ts +0 -6
  315. package/dist/artifacts.js +0 -9
  316. package/dist/auth/bearer.d.ts +0 -11
  317. package/dist/auth/bearer.js +0 -50
  318. package/dist/providers/basecamp.js +0 -445
  319. package/dist/providers/linear.js +0 -217
  320. package/dist/providers/mixpanel.js +0 -247
  321. package/dist/providers/revenuecat.js +0 -317
  322. package/dist/providers/stripe.js +0 -253
  323. package/dist/providers/vercel.js +0 -1253
  324. package/dist/storage/file.d.ts +0 -15
  325. package/dist/storage/file.js +0 -455
  326. package/ethos.md +0 -93
  327. package/examples/worker/src/cloudflare-kv.ts +0 -43
  328. package/examples/worker/src/d1-activity-row.ts +0 -155
  329. package/examples/worker/src/d1-activity.ts +0 -147
  330. package/examples/worker/src/d1-storage.ts +0 -138
  331. package/examples/worker/src/r2-artifact-blobs.ts +0 -29
  332. package/templates/node/src/file-activity.ts +0 -182
@@ -1,9 +1,12 @@
1
1
  import { auth, OAuthError, UnauthorizedError } from "@modelcontextprotocol/client";
2
- import { redirectSafeFetch } from "../connectors/remote-mcp.js";
2
+ import { byteReadResponse } from "../byte-read-response.js";
3
+ import { sentSecretsFor, sentSecretsFetch, shortSecretWarning } from "../sent-secrets.js";
3
4
  import { ConnectorCallError, msg } from "../errors.js";
4
- import { oauthPartitionFor, retainOAuthPartition, retainingOAuthPartition, } from "../oauth-partition.js";
5
+ import { oauthPartitionFor, retainOAuthPartition, retainingOAuthPartition } from "../oauth-partition.js";
5
6
  import { oauthSealerFor } from "../oauth-sealing.js";
6
- import { assertOAuthScope, authorizingContext, KvOAuthProvider, LEGACY_GENERATION, refreshCoordinatorsByPartition, } from "./downstream-oauth.js";
7
+ import { describeFailure } from "../operator-record.js";
8
+ import { assertOAuthScope, authorizingContext, KvOAuthProvider, refreshCoordinatorsByPartition, } from "./downstream-oauth.js";
9
+ import { trackRemoteClientRequest } from "./downstream-client-metadata.js";
7
10
  import { trackOAuthStartReset } from "./oauth-start-reset.js";
8
11
  /**
9
12
  * Downstream OAuth for a hand-written `api()` connector: the authorization
@@ -11,8 +14,8 @@ import { trackOAuthStartReset } from "./oauth-start-reset.js";
11
14
  * declares, rather than ones a server advertises.
12
15
  *
13
16
  * Everything that makes the `remoteMcp()` grant safe is reused, not copied.
14
- * `StaticOAuthProvider` is a `KvOAuthProvider` — the same epochs, sealing,
15
- * owner partitions, generation fence, reset lineage, and refresh-failure
17
+ * `StaticOAuthProvider` is a `KvOAuthProvider` — the same grant record and
18
+ * consents, sealing, owner partitions, epoch fence, and refresh-failure
16
19
  * verdicts — whose discovery state and client are constants, and the SDK's
17
20
  * own `auth()` drives it through the same `OAuthRefreshCoordinator`. What a
18
21
  * static configuration subtracts is the network learning: no RFC 9728 or 8414
@@ -40,15 +43,9 @@ const RESERVED_TOKEN_HEADERS = new Set([
40
43
  ]);
41
44
  /** An RFC 9110 field-name token. */
42
45
  const HEADER_NAME = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;
43
- const CLIENT_AUTHENTICATION = new Set([
44
- "client_secret_basic",
45
- "client_secret_post",
46
- "none",
47
- ]);
46
+ const CLIENT_AUTHENTICATION = new Set(["client_secret_basic", "client_secret_post", "none"]);
48
47
  function isLoopback(hostname) {
49
- return (hostname === "localhost" ||
50
- hostname === "127.0.0.1" ||
51
- hostname === "[::1]");
48
+ return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "[::1]";
52
49
  }
53
50
  /** An absolute HTTPS URL (or HTTP on loopback), without credentials or fragment. */
54
51
  function configuredUrl(id, option, value) {
@@ -82,13 +79,11 @@ function settingsFor(id, config) {
82
79
  if (typeof config.clientId !== "string" || config.clientId.trim() === "") {
83
80
  throw new Error(`[connecta] connector "${id}" oauth.clientId must be a non-empty string.`);
84
81
  }
85
- if (config.clientSecret !== undefined &&
86
- (typeof config.clientSecret !== "string" || config.clientSecret === "")) {
82
+ if (config.clientSecret !== undefined && (typeof config.clientSecret !== "string" || config.clientSecret === "")) {
87
83
  // Usually an unset environment variable read as "". Never echo it.
88
84
  throw new Error(`[connecta] connector "${id}" oauth.clientSecret must be a non-empty string when set.`);
89
85
  }
90
- const clientAuthentication = config.tokenEndpointAuthMethod ??
91
- (config.clientSecret !== undefined ? "client_secret_basic" : "none");
86
+ const clientAuthentication = config.tokenEndpointAuthMethod ?? (config.clientSecret !== undefined ? "client_secret_basic" : "none");
92
87
  if (!CLIENT_AUTHENTICATION.has(clientAuthentication)) {
93
88
  throw new Error(`[connecta] connector "${id}" oauth.tokenEndpointAuthMethod must be ` +
94
89
  '"client_secret_basic", "client_secret_post", or "none".');
@@ -98,8 +93,7 @@ function settingsFor(id, config) {
98
93
  'tokenEndpointAuthMethod "none"; a public client sends no secret.');
99
94
  }
100
95
  if (clientAuthentication !== "none" && config.clientSecret === undefined) {
101
- throw new Error(`[connecta] connector "${id}" oauth.tokenEndpointAuthMethod ` +
102
- `"${clientAuthentication}" needs a clientSecret.`);
96
+ throw new Error(`[connecta] connector "${id}" oauth.tokenEndpointAuthMethod ` + `"${clientAuthentication}" needs a clientSecret.`);
103
97
  }
104
98
  assertOAuthScope(id, config.scope);
105
99
  const authorizationParams = [];
@@ -119,8 +113,7 @@ function settingsFor(id, config) {
119
113
  throw new Error(`[connecta] connector "${id}" oauth.tokenRequestHeaders has an invalid header name.`);
120
114
  }
121
115
  if (RESERVED_TOKEN_HEADERS.has(name.toLowerCase())) {
122
- throw new Error(`[connecta] connector "${id}" oauth.tokenRequestHeaders may not set ` +
123
- `"${name}"; the token request owns it.`);
116
+ throw new Error(`[connecta] connector "${id}" oauth.tokenRequestHeaders may not set ` + `"${name}"; the token request owns it.`);
124
117
  }
125
118
  if (typeof value !== "string" || value.includes("\r") || value.includes("\n") || value.includes("\0")) {
126
119
  throw new Error(`[connecta] connector "${id}" oauth.tokenRequestHeaders.${name} must be a single-line string.`);
@@ -170,7 +163,7 @@ class StaticOAuthProvider extends KvOAuthProvider {
170
163
  super(connectorId, ctx.storage, `${ctx.baseUrl}/oauth/callback/${connectorId}`, coordinator, ctx.allowAuthorization === true, oauthSealerFor(ctx), ctx.signal,
171
164
  // No client binding: the client is configuration, so there is no
172
165
  // registration for a restart to carry forward or discard.
173
- undefined, (reset) => trackOAuthStartReset(ctx.requestScope ?? ctx, reset), undefined, settings.scope);
166
+ undefined, (reset) => trackOAuthStartReset(ctx.requestScope ?? ctx, reset), undefined, settings.scope, undefined, { secrets: sentSecretsFor(ctx) });
174
167
  this.settings = settings;
175
168
  }
176
169
  /**
@@ -182,14 +175,19 @@ class StaticOAuthProvider extends KvOAuthProvider {
182
175
  const { settings } = this;
183
176
  return {
184
177
  client_id: settings.clientId,
185
- ...(settings.clientSecret !== undefined
186
- ? { client_secret: settings.clientSecret }
187
- : {}),
178
+ ...(settings.clientSecret !== undefined ? { client_secret: settings.clientSecret } : {}),
188
179
  redirect_uris: [this.redirectUrl],
189
180
  token_endpoint_auth_method: settings.clientAuthentication,
190
181
  issuer: settings.identity,
191
182
  };
192
183
  }
184
+ configuredAuthorizationBinding() {
185
+ return {
186
+ issuer: this.settings.identity,
187
+ clientId: this.settings.clientId,
188
+ tokenEndpoint: this.settings.tokenEndpoint.href,
189
+ };
190
+ }
193
191
  async saveClientInformation() {
194
192
  // Nothing to save: the SDK only saves a client it registered or stamped.
195
193
  }
@@ -262,36 +260,45 @@ export function staticOAuth(id, config) {
262
260
  const callbackProviders = new WeakMap();
263
261
  const rejectedScopes = new WeakSet();
264
262
  const scopeOf = (ctx) => ctx.requestScope ?? ctx;
265
- const providerFor = (ctx) => new StaticOAuthProvider(id, ctx, coordinatorFor(ctx), settings);
263
+ const warnShortSecret = shortSecretWarning();
264
+ const providerFor = (ctx) => {
265
+ warnShortSecret(settings.clientSecret, ctx.logger);
266
+ return new StaticOAuthProvider(id, ctx, coordinatorFor(ctx), settings);
267
+ };
266
268
  /**
267
269
  * The global `fetch`, read per request, with the configured token-request
268
270
  * headers laid over the SDK's own. With discovery and registration answered
269
271
  * from configuration the token endpoint is the only URL the SDK fetches here;
270
272
  * the check keeps the headers on it even if that ever changes.
271
273
  */
272
- const tokenEndpointFetch = (input, init = {}) => {
274
+ const tokenEndpointFetch = async (ctx, input, init = {}) => {
273
275
  const url = new URL(input);
274
- if (settings.tokenRequestHeaders.length === 0 ||
275
- `${url.origin}${url.pathname}` !== settings.identity) {
276
- return fetch(input, init);
276
+ if (settings.tokenRequestHeaders.length === 0 || `${url.origin}${url.pathname}` !== settings.identity) {
277
+ trackRemoteClientRequest(sentSecretsFor(ctx), input, init);
278
+ return byteReadResponse(await sentSecretsFetch(ctx)(input, { ...init, redirect: "manual" }));
277
279
  }
278
280
  const headers = new Headers(init.headers);
279
281
  for (const [name, value] of settings.tokenRequestHeaders)
280
282
  headers.set(name, value);
281
- return fetch(input, { ...init, headers });
283
+ for (const [, value] of settings.tokenRequestHeaders)
284
+ sentSecretsFor(ctx).header(value);
285
+ trackRemoteClientRequest(sentSecretsFor(ctx), input, { ...init, headers });
286
+ return byteReadResponse(await sentSecretsFetch(ctx)(input, { ...init, headers, redirect: "manual" }));
282
287
  };
283
288
  /**
284
289
  * The SDK's `auth()` over this provider. The token endpoint is fetched
285
- * through the refresh coordinator — rotation, coalescing, and dead-versus-
286
- * outage verdicts — above a fetch that refuses every redirect, as the
287
- * `remoteMcp()` default does.
290
+ * through the refresh coordinator, with permanent spent fingerprints,
291
+ * rotation commits, and re-consent verdicts. Credential-bearing requests
292
+ * never follow redirects in either OAuth adapter.
288
293
  */
289
294
  const runAuth = (provider, ctx, exchange) => auth(provider, {
290
295
  serverUrl: settings.serverUrl,
291
296
  ...exchange,
292
- fetchFn: coordinatorFor(ctx).coordinatedFetch(provider, redirectSafeFetch(id, "none", tokenEndpointFetch), ctx.signal),
297
+ fetchFn: coordinatorFor(ctx).coordinatedFetch(provider, (input, init) => tokenEndpointFetch(ctx, input, init), ctx.signal, ctx.defer),
293
298
  });
294
- const authRequiredError = (cause) => new ConnectorCallError("auth_required", `Connector "${id}" requires authorization — call authorize_connector({ connector: "${id}" }) and open the returned URL.`, cause !== undefined ? { cause } : undefined);
299
+ // No cause, as in remoteMcp(): the `UnauthorizedError` class is the whole
300
+ // verdict, and a logger rendering the chain renders whatever it says.
301
+ const authRequiredError = () => new ConnectorCallError("downstream_oauth_required", `Connector "${id}" requires authorization — call authorize_connector({ connector: "${id}" }) and open the returned URL.`);
295
302
  const disconnectedMessage = `Connector "${id}" was disconnected by an operator — explicitly start authorization to reconnect it.`;
296
303
  /**
297
304
  * One request through the owner's grant. A 401 earns exactly one recovery:
@@ -314,20 +321,23 @@ export function staticOAuth(id, config) {
314
321
  throw new ConnectorCallError("connector_call_failed", `Connector "${id}": a request through ctx.oauth.fetch may not set ` +
315
322
  "Authorization; the connector's grant owns it.", { retryable: false });
316
323
  }
317
- const signal = init.signal && ctx.signal
318
- ? AbortSignal.any([init.signal, ctx.signal])
319
- : (init.signal ?? ctx.signal);
320
- const send = (accessToken) => {
324
+ const signal = init.signal && ctx.signal ? AbortSignal.any([init.signal, ctx.signal]) : (init.signal ?? ctx.signal);
325
+ const send = async (accessToken) => {
326
+ sentSecretsFor(ctx).add(accessToken);
321
327
  const sent = new Headers(headers);
322
328
  sent.set("Authorization", `Bearer ${accessToken}`);
323
329
  // The token rides to a declared origin and no further: a redirect is
324
330
  // handed back to the handler unfollowed rather than re-sent anywhere.
325
- return fetch(url, {
331
+ // The handler reads the answer with `.json()` or `.text()`, which read
332
+ // bytes here: workerd quotes a text read's non-text Content-Type in
333
+ // its own log, out of the handler's reach.
334
+ const response = await sentSecretsFetch(ctx)(url, {
326
335
  ...init,
327
336
  headers: sent,
328
337
  redirect: "manual",
329
338
  ...(signal ? { signal } : {}),
330
339
  });
340
+ return byteReadResponse(response);
331
341
  };
332
342
  if (rejectedScopes.has(scopeOf(ctx)))
333
343
  throw authRequiredError();
@@ -336,13 +346,12 @@ export function staticOAuth(id, config) {
336
346
  if (signal?.aborted)
337
347
  throw signal.reason;
338
348
  const provider = providerFor(ctx);
339
- const generation = await provider.generation();
340
- if (provider.isOperatorDisconnectedGeneration(generation)) {
341
- throw new ConnectorCallError("auth_required", disconnectedMessage);
349
+ // Bind this call's reads and writes to the live epoch: a reset landing
350
+ // meanwhile fails the call rather than handing it another flow's grant.
351
+ const epoch = await provider.beginFlow();
352
+ if (provider.isOperatorDisconnectedEpoch(epoch)) {
353
+ throw new ConnectorCallError("downstream_oauth_required", disconnectedMessage);
342
354
  }
343
- // A refresh this call triggers writes into the epoch it started under,
344
- // so a reset landing meanwhile leaves its rotation unreadable.
345
- provider.captureGeneration(generation);
346
355
  const tokens = await provider.tokens(issuerContext);
347
356
  if (!tokens)
348
357
  throw authRequiredError();
@@ -373,14 +382,7 @@ export function staticOAuth(id, config) {
373
382
  catch (error) {
374
383
  if (error instanceof UnauthorizedError) {
375
384
  rejectedScopes.add(scopeOf(ctx));
376
- throw authRequiredError(error);
377
- }
378
- // The coordinator's answer while another request is still committing
379
- // a rotation it redeemed. The SDK rethrows it untouched; it is a
380
- // moment, not a verdict on the grant.
381
- if (error instanceof OAuthError && error.code === "temporarily_unavailable") {
382
- throw new ConnectorCallError("unavailable", `Connector "${id}" could not refresh its OAuth grant while another ` +
383
- "request was saving one. The grant is kept; retry.", { cause: error });
385
+ throw authRequiredError();
384
386
  }
385
387
  throw error;
386
388
  }
@@ -396,13 +398,14 @@ export function staticOAuth(id, config) {
396
398
  };
397
399
  }
398
400
  // A status read never starts authorization: this provider cannot, and
399
- // nothing here calls the SDK. A stored grant is healthy until a call
400
- // says otherwise — the downstream is asked nothing.
401
+ // nothing here calls the SDK. A stored grant for the configured token
402
+ // endpoint is healthy until a call says otherwise — the downstream is
403
+ // asked nothing.
401
404
  const provider = providerFor(ctx);
402
405
  if (await provider.operatorDisconnected()) {
403
406
  return { state: "auth_required", message: disconnectedMessage };
404
407
  }
405
- return (await provider.tokens())
408
+ return (await provider.tokens(issuerContext))
406
409
  ? { state: "ok" }
407
410
  : { state: "auth_required", message: AUTH_REQUIRED_MESSAGE };
408
411
  };
@@ -434,23 +437,16 @@ export function staticOAuth(id, config) {
434
437
  if (ctx.signal?.aborted)
435
438
  throw ctx.signal.reason;
436
439
  try {
437
- let generation = await provider.generation();
438
- // Born modern. The legacy generation exists so a `remoteMcp()` grant
439
- // from before epochs survives an upgrade; this connector has no such
440
- // past, and a flow started there writes an untimed pending URL that a
441
- // second Connect could not hand back.
442
- if (generation === LEGACY_GENERATION)
443
- generation = await provider.bumpGeneration();
444
- provider.captureGeneration(generation);
445
- // An issuer-bound read: a grant from a since-repointed token endpoint
446
- // is fenced here, before it could be reported healthy.
440
+ // A grant from a since-repointed token endpoint reads as absent here,
441
+ // and the consent this start begins replaces it.
442
+ await provider.beginFlow();
447
443
  if (await provider.tokens(issuerContext)) {
448
444
  return { state: "ok", message: "Already authorized — connection is healthy." };
449
445
  }
450
446
  if ((await runAuth(provider, ctx)) === "AUTHORIZED") {
451
447
  return { state: "ok", message: "Already authorized — connection is healthy." };
452
448
  }
453
- const authorizationUrl = await provider.pendingAuthorizationUrl();
449
+ const authorizationUrl = await provider.consentUrl();
454
450
  return {
455
451
  state: "auth_required",
456
452
  ...(authorizationUrl !== undefined ? { authorizationUrl } : {}),
@@ -461,7 +457,13 @@ export function staticOAuth(id, config) {
461
457
  if (error instanceof UnauthorizedError) {
462
458
  return { state: "auth_required", message: AUTH_REQUIRED_MESSAGE };
463
459
  }
464
- return { state: "error", message: msg(error) };
460
+ // Reaches the agent through authorize_connector. Text already in
461
+ // connecta's words passes (token responses are rebuilt before the SDK
462
+ // reads them); anything else is told from its record.
463
+ return {
464
+ state: "error",
465
+ message: error instanceof ConnectorCallError || error instanceof OAuthError ? msg(error) : describeFailure(id, error),
466
+ };
465
467
  }
466
468
  };
467
469
  const disconnectAuth = async (ctx) => {
@@ -480,7 +482,14 @@ export function staticOAuth(id, config) {
480
482
  callbackProviders.delete(scopeOf(ctx));
481
483
  const authorizationCode = callbackParams?.get("code") ?? code;
482
484
  const iss = callbackParams?.get("iss") ?? undefined;
485
+ // Every exchange names and claims its own consent, including without PKCE.
486
+ const consent = callbackParams?.get("state") ?? null;
487
+ if (!consent || !(await provider.verifyCallbackState(consent))) {
488
+ throw new ConnectorCallError("connector_call_failed", `Connector "${id}" authorization callback matches no pending consent; nothing was exchanged.`);
489
+ }
490
+ provider.validateCallbackIssuer(callbackParams?.get("iss") ?? null);
483
491
  provider.exchanging(authorizationCode);
492
+ await provider.bindFlow();
484
493
  const result = await runAuth(provider, ctx, {
485
494
  authorizationCode,
486
495
  ...(iss !== undefined ? { iss } : {}),
@@ -488,7 +497,6 @@ export function staticOAuth(id, config) {
488
497
  if (result !== "AUTHORIZED") {
489
498
  throw new UnauthorizedError("Failed to authorize");
490
499
  }
491
- await provider.clearPending();
492
500
  rejectedScopes.delete(scopeOf(ctx));
493
501
  };
494
502
  return {
@@ -496,6 +504,20 @@ export function staticOAuth(id, config) {
496
504
  startAuth: retainingOAuthPartition(startAuth, 0),
497
505
  disconnectAuth: retainingOAuthPartition(disconnectAuth, 0),
498
506
  verifyState: retainingOAuthPartition(verifyState, 1),
507
+ verifyCallbackIssuer: retainingOAuthPartition(async (issuer, ctx) => {
508
+ const provider = callbackProviders.get(scopeOf(ctx));
509
+ if (!provider)
510
+ return false;
511
+ provider.validateCallbackIssuer(issuer);
512
+ return true;
513
+ }, 1),
514
+ consumeAuthError: retainingOAuthPartition(async (ctx) => {
515
+ const provider = callbackProviders.get(scopeOf(ctx));
516
+ callbackProviders.delete(scopeOf(ctx));
517
+ if (!provider)
518
+ throw new ConnectorCallError("connector_call_failed", "OAuth error callback matches no pending consent.");
519
+ await provider.consumeAuthError();
520
+ }, 0),
499
521
  finishAuth: retainingOAuthPartition(finishAuth, 1),
500
522
  access: (ctx) => ({
501
523
  fetch: (input, init) => authorizedFetch(ctx, input, init),
@@ -0,0 +1,288 @@
1
+ import { enableAuthorizationHandoffs } from "./authorization-handoff.js";
2
+ import { createRequestStateCodec, inputRequired, ProtocolError, INVALID_PARAMS, PROTOCOL_VERSION_META_KEY, CLIENT_CAPABILITIES_META_KEY, } from "@modelcontextprotocol/server";
3
+ import { Effect } from "effect";
4
+ import { runEdge, withDeadlineEffect } from "./runtime/run.js";
5
+ import { closeScope } from "./runtime/connector-scope.js";
6
+ import { storedCredentialShape } from "./credential-rules.js";
7
+ import { authRecoveryFacts, bindReplayReads, classificationDigest } from "./invocation-auth.js";
8
+ import { CatalogService } from "./catalog-service.js";
9
+ import { DownstreamElicitation, MAX_RELAY_STATE_CHARS, } from "./downstream-input.js";
10
+ import { canonicalState as canonical, requestDigest as digest, stateObject as object, invalidRequestState as invalidState, REQUEST_STATE_TTL_MS as TTL_MS, MAX_INPUT_ROUNDS as MAX_ROUNDS, } from "./request-state.js";
11
+ const INPUT_KEY = "connecta_auth";
12
+ const MESSAGE = "Connect this service in your browser, then retry the request.";
13
+ function failure(code, message) {
14
+ const structuredContent = { ok: false, error: { code, message, retryable: false } };
15
+ return { content: [{ type: "text", text: JSON.stringify(structuredContent) }], structuredContent, isError: true };
16
+ }
17
+ /** Request-local auth recovery at the MCP boundary, never inside a guest provider. */
18
+ export class AuthElicitation {
19
+ options;
20
+ codec;
21
+ downstream;
22
+ stateCodec() {
23
+ const key = this.options.vault?.requestStateKey;
24
+ if (!key)
25
+ return invalidState();
26
+ return (this.codec ??= key.call(this.options.vault).then((bytes) => createRequestStateCodec({
27
+ key: bytes,
28
+ ttlSeconds: TTL_MS / 1000,
29
+ bind: () => canonical({ principal: this.options.principal, endpoint: this.options.endpoint }),
30
+ })));
31
+ }
32
+ constructor(options) {
33
+ this.options = options;
34
+ this.downstream = new DownstreamElicitation({
35
+ ...options,
36
+ mint: async (state, context) => (await this.stateCodec()).mint(state, context),
37
+ });
38
+ }
39
+ /** The SDK runs this before any tool handler, including non-auth tools. */
40
+ async verify(wire, context) {
41
+ const { vault, endpoint, principal, registry, canManage } = this.options;
42
+ if (!vault?.requestStateKey || wire.length > MAX_RELAY_STATE_CHARS)
43
+ return invalidState();
44
+ const state = await (await this.stateCodec()).verify(wire, context);
45
+ if (object(state) && state.version === 2 && state.kind === "downstream")
46
+ return this.downstream.verify(state, context);
47
+ if (wire.length > 8192)
48
+ return invalidState();
49
+ if (!object(state) ||
50
+ state.version !== 1 ||
51
+ !principal ||
52
+ state.principal !== principal ||
53
+ state.endpoint !== endpoint ||
54
+ typeof state.connector !== "string" ||
55
+ !registry.getConnector(state.connector) ||
56
+ !canManage(state.connector) ||
57
+ !Number.isSafeInteger(state.expiresAt) ||
58
+ state.expiresAt <= Date.now() ||
59
+ state.expiresAt > Date.now() + TTL_MS ||
60
+ !Number.isInteger(state.round) ||
61
+ state.round < 1 ||
62
+ state.round > MAX_ROUNDS ||
63
+ !["call_tool", "call_destructive_tool", "execute_code", "authorize_connector"].includes(String(state.tool)) ||
64
+ typeof state.digest !== "string" ||
65
+ !/^[a-f0-9]{64}$/.test(state.digest) ||
66
+ !Array.isArray(state.browserNonces) ||
67
+ state.browserNonces.length !== state.round ||
68
+ !state.browserNonces.every((nonce) => typeof nonce === "string" && /^[a-f0-9-]{36}$/.test(nonce)) ||
69
+ !Array.isArray(state.reads) ||
70
+ !state.reads.every((read) => object(read) &&
71
+ typeof read.address === "string" &&
72
+ typeof read.digest === "string" &&
73
+ /^[a-f0-9]{64}$/.test(read.digest)) ||
74
+ (state.address !== undefined && typeof state.address !== "string"))
75
+ return invalidState();
76
+ if (context.mcpReq.method !== "tools/call" || context.http?.req?.headers.get("Mcp-Name") !== state.tool)
77
+ return invalidState();
78
+ return state;
79
+ }
80
+ async browserProgress(state) {
81
+ let started;
82
+ let claimed;
83
+ let completed = false;
84
+ let pending = false;
85
+ for (const nonce of state.browserNonces) {
86
+ const progress = await this.options.linkProgress(state.connector, nonce);
87
+ if (progress === "started")
88
+ started = nonce;
89
+ if (progress === "claimed")
90
+ claimed = nonce;
91
+ completed ||= progress === "started";
92
+ pending ||= progress === "claimed";
93
+ }
94
+ const predecessor = started ?? claimed;
95
+ return { ...(predecessor ? { predecessor } : {}), completed: completed && !pending };
96
+ }
97
+ async connected(id, context) {
98
+ const { registry, publicUrl, vault, principal, defer } = this.options;
99
+ const connector = registry.getConnector(id);
100
+ const scope = {};
101
+ const signal = AbortSignal.any([context.mcpReq.signal, this.options.requestSignal]);
102
+ try {
103
+ return await runEdge(withDeadlineEffect((deadline) => Effect.gen(function* () {
104
+ const ctx = registry.contextFor(id, publicUrl, scope, { signal: deadline });
105
+ yield* Effect.addFinalizer(() => closeScope(connector, ctx, defer));
106
+ if (connector.credential) {
107
+ const metadata = yield* Effect.promise(() => vault.metadata(id, connector.authScope === "personal" ? principal : undefined));
108
+ if (storedCredentialShape(connector.credential, metadata?.fields ?? null).state !== "valid")
109
+ return false;
110
+ }
111
+ if (!connector.status)
112
+ return Boolean(connector.credential);
113
+ return ((yield* Effect.promise(() => registry.statusFor(id, publicUrl, scope, { signal: deadline }))).state ===
114
+ "ok");
115
+ }).pipe(Effect.scoped), { signal, timeoutMs: 5000, timeoutError: new Error("Auth status check timed out") }));
116
+ }
117
+ catch {
118
+ return false;
119
+ }
120
+ }
121
+ async run(tool, args, context, operation) {
122
+ this.downstream.bind(context);
123
+ const prior = context.mcpReq.requestState();
124
+ if (prior?.version === 2)
125
+ return this.downstream.resume(prior, tool, args, context, operation);
126
+ const previous = prior;
127
+ const requestDigest = previous ? await digest(tool, args) : undefined;
128
+ if (previous && (previous.tool !== tool || previous.digest !== requestDigest || previous.address !== args.address))
129
+ return failure("invalid_request_state", "Invalid or expired requestState");
130
+ const response = context.mcpReq.inputResponses?.[INPUT_KEY];
131
+ if (previous && object(response) && !["accept", "decline", "cancel"].includes(String(response.action))) {
132
+ throw new ProtocolError(INVALID_PARAMS, "Invalid auth elicitation response", {
133
+ reason: "invalid_input_response",
134
+ });
135
+ }
136
+ if (previous && !(await this.options.claimRetry(previous.connector, previous.browserNonces.at(-1)))) {
137
+ return failure("invalid_request_state", "Invalid or expired requestState");
138
+ }
139
+ if (previous && object(response)) {
140
+ if (response.action === "decline")
141
+ return failure("auth_declined", "Connection was declined. The request was not retried.");
142
+ if (response.action === "cancel")
143
+ return failure("auth_cancelled", "Connection was cancelled. The request was not retried.");
144
+ }
145
+ if (previous?.reads.length) {
146
+ const catalog = new CatalogService(this.options.registry, this.options.publicUrl, {
147
+ requestScope: this.options.requestScope,
148
+ requestSignal: AbortSignal.any([context.mcpReq.signal, this.options.requestSignal]),
149
+ });
150
+ // Recheck every entered read before restarting any part of a program.
151
+ // A trusted pool cannot authorize replay of a newly classified write.
152
+ for (const read of previous.reads) {
153
+ const current = await catalog.resolveTool(read.address, { signal: context.mcpReq.signal });
154
+ if (!current.ok ||
155
+ current.resolved.classificationFresh !== true ||
156
+ current.resolved.definition.classification !== "read" ||
157
+ (await classificationDigest(current.resolved.definition)) !== read.digest) {
158
+ const structuredContent = {
159
+ ok: false,
160
+ error: {
161
+ code: "auth_replay_refused",
162
+ retryable: false,
163
+ reconciliationRequired: true,
164
+ message: "An entered read no longer has the same fresh classification. Reconcile its target before starting a new request.",
165
+ authorizationUrl: (await this.options.connectLink(previous.connector)).url,
166
+ },
167
+ };
168
+ return {
169
+ isError: true,
170
+ structuredContent,
171
+ content: [{ type: "text", text: JSON.stringify(structuredContent) }],
172
+ };
173
+ }
174
+ }
175
+ bindReplayReads(this.options.requestScope, previous.reads);
176
+ }
177
+ const browser = previous && tool === "authorize_connector" ? await this.browserProgress(previous) : undefined;
178
+ if (browser?.completed && previous) {
179
+ // Retire unspent links before the status read. A browser claim racing
180
+ // these CASes either wins and is observed as pending, or is refused.
181
+ await this.options.closeLinks(previous.connector, previous.browserNonces);
182
+ if ((await this.browserProgress(previous)).completed && (await this.connected(previous.connector, context))) {
183
+ const structuredContent = { connector: previous.connector, status: "ok" };
184
+ return { structuredContent, content: [{ type: "text", text: JSON.stringify(structuredContent) }] };
185
+ }
186
+ }
187
+ const envelope = context.mcpReq.envelope;
188
+ const capabilities = envelope?.[CLIENT_CAPABILITIES_META_KEY];
189
+ const elicitation = object(capabilities) ? capabilities.elicitation : undefined;
190
+ const capable = envelope?.[PROTOCOL_VERSION_META_KEY] === "2026-07-28" &&
191
+ object(elicitation) &&
192
+ Object.hasOwn(elicitation, "url") &&
193
+ object(elicitation.url);
194
+ enableAuthorizationHandoffs(this.options.requestScope, !capable);
195
+ let result = await operation();
196
+ const relayed = await this.downstream.finish(tool, args, context, result, previous);
197
+ if (relayed !== result)
198
+ return relayed;
199
+ const error = result.structuredContent?.error;
200
+ const authFailure = result.isError && object(error) && (error.code === "auth_required" || error.code === "downstream_oauth_required");
201
+ const explicit = tool === "authorize_connector" &&
202
+ ["oauth", "operator_config"].includes(String(result.structuredContent?.recovery));
203
+ if (!authFailure && !explicit)
204
+ return result;
205
+ const id = authFailure ? error.connector : explicit ? args.connector : undefined;
206
+ // Every accepted retry owns new request-local facts. Handler-authored
207
+ // errors and program write summaries cannot establish replay eligibility.
208
+ const facts = await authRecoveryFacts(this.options.requestScope, String(id));
209
+ const blocked = !explicit && (!facts.eligible || facts.writeEntered);
210
+ if (authFailure && (facts.unsafeEntered || (tool !== "call_tool" && !facts.eligible))) {
211
+ const structuredContent = {
212
+ ...result.structuredContent,
213
+ error: {
214
+ ...error,
215
+ retryable: false,
216
+ reconciliationRequired: true,
217
+ retry: "This call may have partially run. Reconcile its target before retrying after the operator completes recovery.",
218
+ },
219
+ };
220
+ result = { ...result, structuredContent, content: [{ type: "text", text: JSON.stringify(structuredContent) }] };
221
+ }
222
+ const { registry, vault, principal, publicUrl, canManage, unavailable } = this.options;
223
+ const connector = typeof id === "string" ? registry.getConnector(id) : undefined;
224
+ const recoverable = connector?.startAuth || (connector?.credential && this.options.credentialUi);
225
+ if (typeof id !== "string" ||
226
+ !/^[a-z0-9_-]+$/.test(id) ||
227
+ !recoverable ||
228
+ !principal ||
229
+ !publicUrl ||
230
+ unavailable ||
231
+ !canManage(id))
232
+ return result;
233
+ // Incapable hosts already received the shared explicit-authorization handoff.
234
+ if (!capable)
235
+ return result;
236
+ if (blocked || !vault?.requestStateKey || !vault.seal || !vault.open) {
237
+ if (!authFailure)
238
+ return result;
239
+ const structuredContent = {
240
+ ...result.structuredContent,
241
+ error: {
242
+ ...result.structuredContent?.error,
243
+ authorizationUrl: error.authorizationUrl ?? (await this.options.connectLink(id)).url,
244
+ },
245
+ };
246
+ return { ...result, structuredContent, content: [{ type: "text", text: JSON.stringify(structuredContent) }] };
247
+ }
248
+ if (previous && previous.round >= MAX_ROUNDS)
249
+ return failure("auth_round_limit", "Connection is still required after three prompts. Connect explicitly, then start a new request.");
250
+ // Once a verified browser has started this attempt, retries continue it.
251
+ // A pending consent must never be reset by another force=true prompt.
252
+ const forced = explicit && args.force === true;
253
+ const link = await this.options.connectLink(id, forced && !browser?.predecessor, forced ? browser?.predecessor : undefined);
254
+ const state = {
255
+ version: 1,
256
+ principal,
257
+ endpoint: this.options.endpoint,
258
+ connector: id,
259
+ tool,
260
+ ...(typeof args.address === "string" ? { address: args.address } : {}),
261
+ digest: requestDigest ?? (await digest(tool, args)),
262
+ round: (previous?.round ?? 0) + 1,
263
+ expiresAt: previous?.expiresAt ?? Date.now() + TTL_MS,
264
+ browserNonces: [...(previous?.browserNonces ?? []), link.nonce],
265
+ reads: [...new Map([...(previous?.reads ?? []), ...facts.reads].map((read) => [read.address, read])).values()],
266
+ };
267
+ const wire = await (await this.stateCodec()).mint(state, context);
268
+ if (wire.length > 8192) {
269
+ const structuredContent = {
270
+ ...result.structuredContent,
271
+ error: {
272
+ ...result.structuredContent?.error,
273
+ authorizationUrl: link.url,
274
+ },
275
+ };
276
+ return { ...result, structuredContent, content: [{ type: "text", text: JSON.stringify(structuredContent) }] };
277
+ }
278
+ return inputRequired({
279
+ inputRequests: {
280
+ [INPUT_KEY]: inputRequired.elicitUrl({
281
+ message: MESSAGE,
282
+ url: link.url,
283
+ }),
284
+ },
285
+ requestState: wire,
286
+ });
287
+ }
288
+ }