@zackbart/connecta 0.34.1 → 0.35.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 (429) hide show
  1. package/CHANGELOG.md +363 -0
  2. package/README.md +62 -106
  3. package/SECURITY.md +3 -2
  4. package/bin/connecta.mjs +41 -16
  5. package/bin/version.mjs +1 -1
  6. package/dist/access-tokens.d.ts +2 -1
  7. package/dist/access-tokens.js +66 -30
  8. package/dist/activity.d.ts +1 -1
  9. package/dist/activity.js +2 -0
  10. package/dist/agent-config.d.ts +23 -0
  11. package/dist/agent-config.js +59 -0
  12. package/dist/agent-description.js +16 -0
  13. package/dist/agent-identity.js +4 -4
  14. package/dist/agent-observation.js +39 -0
  15. package/dist/auth/clerk-sdk/client.d.ts +2 -0
  16. package/dist/auth/clerk-sdk/generated.js +554 -563
  17. package/dist/auth/clerk-transport.js +70 -3
  18. package/dist/auth/clerk.d.ts +13 -0
  19. package/dist/auth/clerk.js +653 -246
  20. package/dist/auth/cloudflare-access.js +46 -42
  21. package/dist/auth/downstream-client-metadata.js +2 -2
  22. package/dist/auth/downstream-oauth.js +2051 -1160
  23. package/dist/auth/downstream-revocation.js +88 -88
  24. package/dist/auth/native-call.js +20 -0
  25. package/dist/auth/native.js +184 -0
  26. package/dist/auth/oauth-start-reset.js +11 -5
  27. package/dist/auth/oauth-token-response.js +121 -102
  28. package/dist/auth/oauth-v2-migration.js +97 -78
  29. package/dist/auth/renewable-grant.js +10 -0
  30. package/dist/auth/static-oauth.js +663 -271
  31. package/dist/auth/vercel-agent.js +266 -0
  32. package/dist/auth-admission.js +204 -0
  33. package/dist/auth-elicitation.js +350 -241
  34. package/dist/authorization-handoff.js +164 -90
  35. package/dist/call-admission-context.js +3 -10
  36. package/dist/call-admission.js +2 -0
  37. package/dist/call-classification.js +9 -0
  38. package/dist/call-recovery.js +32 -13
  39. package/dist/canonical-json.js +8 -0
  40. package/dist/catalog-cache.js +369 -213
  41. package/dist/catalog-service.js +659 -530
  42. package/dist/catalog.js +5 -1
  43. package/dist/concurrency.js +0 -25
  44. package/dist/config-defaults.d.ts +1 -0
  45. package/dist/config-defaults.js +1 -0
  46. package/dist/config-schema.d.ts +3 -1
  47. package/dist/config-schema.js +13 -3
  48. package/dist/config.d.ts +40 -6
  49. package/dist/config.js +40 -26
  50. package/dist/connector-caller.d.ts +4 -0
  51. package/dist/connector-caller.js +17 -4
  52. package/dist/connectors/api-connector.d.ts +12 -7
  53. package/dist/connectors/api-connector.js +142 -56
  54. package/dist/connectors/guarded-fetch.d.ts +51 -17
  55. package/dist/connectors/guarded-fetch.js +215 -121
  56. package/dist/connectors/negotiation-cache.js +20 -10
  57. package/dist/connectors/remote-mcp.d.ts +10 -30
  58. package/dist/connectors/remote-mcp.js +955 -563
  59. package/dist/d1.js +10 -243
  60. package/dist/derived-context.js +5 -0
  61. package/dist/describe-config.d.ts +3 -1
  62. package/dist/describe-config.js +2 -0
  63. package/dist/downstream-input.js +347 -260
  64. package/dist/errors.d.ts +87 -6
  65. package/dist/errors.js +114 -4
  66. package/dist/execute.js +87 -50
  67. package/dist/executor-admission.js +3 -0
  68. package/dist/executors/quickjs.js +163 -112
  69. package/dist/guarded-transport.js +50 -0
  70. package/dist/guest-runtime.js +1 -1
  71. package/dist/http/client.js +439 -0
  72. package/dist/http/errors.js +558 -0
  73. package/dist/http/fields.js +49 -0
  74. package/dist/http/hatch-paging.js +318 -0
  75. package/dist/http/hatch.js +1279 -0
  76. package/dist/http/json-schema.js +267 -0
  77. package/dist/http/node-transport.js +241 -0
  78. package/dist/http/oauth.js +162 -0
  79. package/dist/http/paging.js +228 -0
  80. package/dist/http/provider.js +221 -0
  81. package/dist/http/safety.js +82 -0
  82. package/dist/http/services.js +79 -0
  83. package/dist/http/tools.js +383 -0
  84. package/dist/http/vendor-index.js +138 -0
  85. package/dist/http/vendor.js +448 -0
  86. package/dist/http/write-request.js +66 -0
  87. package/dist/http-policy.d.ts +11 -0
  88. package/dist/http-policy.js +45 -0
  89. package/dist/inbound-credential.js +1 -1
  90. package/dist/index.d.ts +2 -2
  91. package/dist/index.js +4 -0
  92. package/dist/invocation-auth.js +30 -19
  93. package/dist/invocation.js +218 -85
  94. package/dist/mcp-result.js +0 -4
  95. package/dist/membership.d.ts +4 -0
  96. package/dist/meta-output.js +1 -1
  97. package/dist/meta-tools.js +531 -406
  98. package/dist/native-recovery.js +52 -0
  99. package/dist/native-tools.js +382 -0
  100. package/dist/node.d.ts +4 -3
  101. package/dist/node.js +45 -8
  102. package/dist/oauth-handoff.js +177 -104
  103. package/dist/oauth-partition.js +6 -20
  104. package/dist/oauth-sealing.js +6 -7
  105. package/dist/operation-vocabulary.js +101 -0
  106. package/dist/operator-record.d.ts +2 -2
  107. package/dist/operator-record.js +17 -4
  108. package/dist/operator-ui/agents.d.ts +5 -4
  109. package/dist/operator-ui/agents.js +17 -7
  110. package/dist/operator-ui/config-summary.js +5 -4
  111. package/dist/operator-ui/connection-map.js +7 -5
  112. package/dist/operator-ui/contract.d.ts +7 -3
  113. package/dist/operator-ui/endpoints.js +4 -13
  114. package/dist/operator-ui/fix-prompts.d.ts +1 -1
  115. package/dist/operator-ui/fix-prompts.js +0 -8
  116. package/dist/operator-ui/generated.js +3 -3
  117. package/dist/operator-ui/model.d.ts +2 -3
  118. package/dist/operator-ui/overview.js +5 -2
  119. package/dist/operator-ui/setup-commands.js +0 -4
  120. package/dist/operator-ui/view.d.ts +2 -25
  121. package/dist/operator-ui/view.js +2 -76
  122. package/dist/program-writes.js +47 -26
  123. package/dist/provider-call-facts.d.ts +115 -0
  124. package/dist/provider-call-facts.js +182 -0
  125. package/dist/provider-call.d.ts +112 -0
  126. package/dist/provider-call.js +271 -0
  127. package/dist/provider.d.ts +65 -31
  128. package/dist/provider.js +116 -39
  129. package/dist/providers/_shared/google/hatch.js +91 -0
  130. package/dist/providers/_shared/google/kit.js +537 -0
  131. package/dist/providers/_shared/google/result-size.js +15 -2
  132. package/dist/providers/_shared/google/service-account.d.ts +2 -1
  133. package/dist/providers/_shared/google/service-account.js +8 -4
  134. package/dist/providers/_shared/google/workspace.d.ts +6 -210
  135. package/dist/providers/_shared/google/workspace.js +6 -425
  136. package/dist/providers/_shared/rest/compact-index.js +86 -0
  137. package/dist/providers/_shared/rest/dispatch.js +3 -23
  138. package/dist/providers/_shared/rest/kit.js +546 -0
  139. package/dist/providers/_shared/rest/operation-index.js +230 -14
  140. package/dist/providers/_shared/rest/response-media.js +11 -0
  141. package/dist/providers/_shared/rest/tools.js +101 -78
  142. package/dist/providers/_shared/rest/value-safety.js +353 -195
  143. package/dist/providers/_shared/sensitive-fields.js +27 -0
  144. package/dist/providers/breeze/api.js +584 -0
  145. package/dist/providers/breeze/index.d.ts +1 -3
  146. package/dist/providers/breeze/index.js +67 -1420
  147. package/dist/providers/breeze/openapi.generated.js +60 -0
  148. package/dist/providers/breeze/skill.generated.js +2 -6
  149. package/dist/providers/breeze/tools.js +696 -0
  150. package/dist/providers/ccb/api.js +496 -0
  151. package/dist/providers/ccb/index.d.ts +15 -37
  152. package/dist/providers/ccb/index.js +77 -1702
  153. package/dist/providers/ccb/lib/constants.d.ts +34 -0
  154. package/dist/providers/ccb/lib/constants.js +74 -0
  155. package/dist/providers/ccb/lib/guide.js +9 -0
  156. package/dist/providers/ccb/lib/verdicts.js +387 -0
  157. package/dist/providers/ccb/openapi.generated.js +397 -0
  158. package/dist/providers/ccb/safety.js +295 -0
  159. package/dist/providers/ccb/skill.generated.js +2 -6
  160. package/dist/providers/ccb/tools.js +853 -0
  161. package/dist/providers/cloudflare/api.js +1066 -0
  162. package/dist/providers/cloudflare/index.d.ts +5 -22
  163. package/dist/providers/cloudflare/index.js +79 -197
  164. package/dist/providers/cloudflare/lib/config.d.ts +16 -0
  165. package/dist/providers/cloudflare/lib/config.js +2 -0
  166. package/dist/providers/cloudflare/lib/discovery.js +29 -0
  167. package/dist/providers/cloudflare/{graphql.js → lib/graphql.js} +1 -1
  168. package/dist/providers/cloudflare/lib/hatch.js +103 -0
  169. package/dist/providers/cloudflare/lib/path-bounds.js +28 -0
  170. package/dist/providers/cloudflare/lib/response-media.js +75 -0
  171. package/dist/providers/cloudflare/openapi.generated.js +4 -3
  172. package/dist/providers/cloudflare/{value-safety.js → safety.js} +487 -0
  173. package/dist/providers/cloudflare/skill.generated.js +2 -10
  174. package/dist/providers/cloudflare/tools.js +360 -0
  175. package/dist/providers/docs/api.js +51 -0
  176. package/dist/providers/docs/index.d.ts +1 -12
  177. package/dist/providers/docs/index.js +53 -1165
  178. package/dist/providers/docs/lib/constants.d.ts +18 -0
  179. package/dist/providers/docs/lib/constants.js +18 -0
  180. package/dist/providers/docs/lib/discovery.js +26 -0
  181. package/dist/providers/docs/lib/requests.js +48 -0
  182. package/dist/providers/docs/lib/schemas.js +168 -0
  183. package/dist/providers/docs/openapi.generated.js +16 -0
  184. package/dist/providers/docs/safety.js +54 -0
  185. package/dist/providers/docs/skill.generated.js +2 -6
  186. package/dist/providers/docs/tools.js +802 -0
  187. package/dist/providers/drive/api.js +177 -0
  188. package/dist/providers/drive/index.d.ts +1 -12
  189. package/dist/providers/drive/index.js +38 -1448
  190. package/dist/providers/drive/lib/constants.d.ts +6 -0
  191. package/dist/providers/drive/lib/constants.js +6 -0
  192. package/dist/providers/drive/lib/contracts.js +532 -0
  193. package/dist/providers/drive/lib/hatch.js +197 -0
  194. package/dist/providers/drive/openapi.generated.js +77 -0
  195. package/dist/providers/drive/safety.js +90 -0
  196. package/dist/providers/drive/skill.generated.js +2 -6
  197. package/dist/providers/drive/tools.js +1290 -0
  198. package/dist/providers/forms/api.js +71 -0
  199. package/dist/providers/forms/index.d.ts +1 -12
  200. package/dist/providers/forms/index.js +49 -1145
  201. package/dist/providers/forms/lib/constants.d.ts +14 -0
  202. package/dist/providers/forms/lib/constants.js +19 -0
  203. package/dist/providers/forms/lib/hatch.js +72 -0
  204. package/dist/providers/forms/openapi.generated.js +23 -0
  205. package/dist/providers/forms/safety.js +34 -0
  206. package/dist/providers/forms/skill.generated.js +2 -8
  207. package/dist/providers/forms/tools.js +1024 -0
  208. package/dist/providers/github/api.js +150 -0
  209. package/dist/providers/github/index.d.ts +4 -5
  210. package/dist/providers/github/index.js +30 -87
  211. package/dist/providers/github/{auth.d.ts → lib/auth.d.ts} +5 -3
  212. package/dist/providers/github/{auth.js → lib/auth.js} +40 -43
  213. package/dist/providers/github/lib/discovery.js +132 -0
  214. package/dist/providers/github/lib/operations.js +58 -0
  215. package/dist/providers/github/{scope.d.ts → lib/scope.d.ts} +2 -0
  216. package/dist/providers/github/{scope.js → lib/scope.js} +25 -1
  217. package/dist/providers/github/openapi.generated.js +1216 -0
  218. package/dist/providers/github/safety.js +216 -0
  219. package/dist/providers/github/skill.generated.js +2 -5
  220. package/dist/providers/github/tools.js +506 -0
  221. package/dist/providers/gmail/api.js +114 -0
  222. package/dist/providers/gmail/index.d.ts +1 -12
  223. package/dist/providers/gmail/index.js +73 -1621
  224. package/dist/providers/gmail/lib/constants.d.ts +13 -0
  225. package/dist/providers/gmail/lib/constants.js +16 -0
  226. package/dist/providers/gmail/lib/index.js +173 -0
  227. package/dist/providers/gmail/lib/schemas.js +176 -0
  228. package/dist/providers/gmail/lib/threading.js +120 -0
  229. package/dist/providers/gmail/openapi.generated.js +92 -0
  230. package/dist/providers/gmail/safety.js +21 -0
  231. package/dist/providers/gmail/skill.generated.js +2 -6
  232. package/dist/providers/gmail/tools.js +1435 -0
  233. package/dist/providers/overflow/api.js +1008 -0
  234. package/dist/providers/overflow/index.d.ts +1 -17
  235. package/dist/providers/overflow/index.js +60 -1488
  236. package/dist/providers/overflow/skill.generated.js +2 -5
  237. package/dist/providers/overflow/tools.js +873 -0
  238. package/dist/providers/planning-center/api.js +467 -0
  239. package/dist/providers/planning-center/index.d.ts +4 -35
  240. package/dist/providers/planning-center/index.js +69 -2445
  241. package/dist/providers/planning-center/lib/constants.d.ts +58 -0
  242. package/dist/providers/planning-center/lib/constants.js +83 -0
  243. package/dist/providers/planning-center/lib/discovery.js +40 -0
  244. package/dist/providers/planning-center/lib/guide.js +39 -0
  245. package/dist/providers/planning-center/lib/json-api.js +83 -0
  246. package/dist/providers/planning-center/lib/verdicts.js +1758 -0
  247. package/dist/providers/planning-center/openapi.generated.js +1764 -0
  248. package/dist/providers/planning-center/safety.js +188 -0
  249. package/dist/providers/planning-center/skill.generated.js +2 -27
  250. package/dist/providers/planning-center/tools.js +1010 -0
  251. package/dist/providers/sheets/api.js +133 -0
  252. package/dist/providers/sheets/index.d.ts +4 -16
  253. package/dist/providers/sheets/index.js +61 -1172
  254. package/dist/providers/sheets/lib/constants.d.ts +14 -0
  255. package/dist/providers/sheets/lib/constants.js +14 -0
  256. package/dist/providers/sheets/lib/hatch.js +163 -0
  257. package/dist/providers/sheets/lib/requests.js +105 -0
  258. package/dist/providers/sheets/lib/schemas.js +138 -0
  259. package/dist/providers/sheets/openapi.generated.js +30 -0
  260. package/dist/providers/sheets/safety.js +27 -0
  261. package/dist/providers/sheets/skill.generated.js +2 -7
  262. package/dist/providers/sheets/tools.js +734 -0
  263. package/dist/providers/slides/api.js +147 -0
  264. package/dist/providers/slides/index.d.ts +2 -10
  265. package/dist/providers/slides/index.js +48 -2555
  266. package/dist/providers/slides/lib/constants.d.ts +11 -0
  267. package/dist/providers/slides/lib/constants.js +11 -0
  268. package/dist/providers/slides/lib/requests.js +60 -0
  269. package/dist/providers/slides/lib/schemas.js +565 -0
  270. package/dist/providers/slides/openapi.generated.js +18 -0
  271. package/dist/providers/slides/skill.generated.js +2 -6
  272. package/dist/providers/slides/tools.js +1968 -0
  273. package/dist/providers/stripe/api.js +401 -0
  274. package/dist/providers/stripe/index.d.ts +5 -27
  275. package/dist/providers/stripe/index.js +74 -210
  276. package/dist/providers/stripe/lib/config.d.ts +4 -0
  277. package/dist/providers/stripe/lib/config.js +2 -0
  278. package/dist/providers/stripe/openapi.generated.js +4 -3
  279. package/dist/providers/stripe/{value-safety.js → safety.js} +5 -5
  280. package/dist/providers/stripe/skill.generated.js +2 -11
  281. package/dist/providers/stripe/tools.js +77 -0
  282. package/dist/providers/tithely/api.js +531 -0
  283. package/dist/providers/tithely/index.d.ts +17 -0
  284. package/dist/providers/tithely/index.js +86 -965
  285. package/dist/providers/tithely/skill.generated.js +2 -5
  286. package/dist/providers/tithely/tools.js +426 -0
  287. package/dist/providers/vercel/api.js +551 -0
  288. package/dist/providers/vercel/index.d.ts +9 -25
  289. package/dist/providers/vercel/index.js +66 -260
  290. package/dist/providers/vercel/lib/config.d.ts +2 -0
  291. package/dist/providers/vercel/lib/config.js +2 -0
  292. package/dist/providers/vercel/lib/index.js +117 -0
  293. package/dist/providers/vercel/lib/logs.js +100 -0
  294. package/dist/providers/vercel/lib/pin.js +270 -0
  295. package/dist/providers/vercel/openapi.generated.js +10 -5
  296. package/dist/providers/vercel/{value-safety.js → safety.js} +247 -38
  297. package/dist/providers/vercel/skill.generated.js +2 -7
  298. package/dist/providers/vercel/tools.js +642 -0
  299. package/dist/redaction-identity.js +47 -0
  300. package/dist/redaction-marker.js +125 -0
  301. package/dist/registry.d.ts +21 -15
  302. package/dist/registry.js +409 -376
  303. package/dist/routes/access-tokens.js +1 -1
  304. package/dist/routes/actor-labels.js +1 -1
  305. package/dist/routes/connect.js +1 -1
  306. package/dist/routes/dispatch.js +103 -0
  307. package/dist/routes/mcp.js +170 -79
  308. package/dist/routes/oauth.js +3 -1
  309. package/dist/routes/operator.js +2 -2
  310. package/dist/routes/shared.d.ts +5 -4
  311. package/dist/routes/shared.js +244 -177
  312. package/dist/routes/skills.js +10 -8
  313. package/dist/routes/ui-config.js +27 -3
  314. package/dist/routes/ui-data.js +21 -20
  315. package/dist/routes/ui.js +1 -1
  316. package/dist/runtime/call-admission.js +0 -26
  317. package/dist/runtime/call-entry.js +30 -0
  318. package/dist/runtime/call.js +556 -0
  319. package/dist/runtime/connector-scope.js +7 -0
  320. package/dist/runtime/credential-probe.js +34 -0
  321. package/dist/runtime/executor.js +40 -0
  322. package/dist/runtime/native-storage.js +63 -0
  323. package/dist/runtime/registry.js +27 -0
  324. package/dist/runtime/request-scope.js +72 -0
  325. package/dist/runtime/run.js +15 -7
  326. package/dist/runtime/shared-read.js +34 -5
  327. package/dist/runtime/storage.js +34 -21
  328. package/dist/sent-secrets.d.ts +194 -37
  329. package/dist/sent-secrets.js +1743 -340
  330. package/dist/server.js +5 -56
  331. package/dist/skills.js +322 -222
  332. package/dist/sqlite.js +15 -14
  333. package/dist/storage/d1-copy.js +255 -0
  334. package/dist/storage/d1-driver.js +15 -0
  335. package/dist/storage/keys.js +28 -0
  336. package/dist/storage/sql-driver.js +88 -0
  337. package/dist/storage/sql-native.js +544 -0
  338. package/dist/storage/sql-schema.d.ts +6 -0
  339. package/dist/storage/sql-schema.js +15 -0
  340. package/dist/storage/sql.d.ts +14 -20
  341. package/dist/storage/sql.js +29 -535
  342. package/dist/timeout.js +0 -26
  343. package/dist/tool-safety.js +20 -10
  344. package/dist/types.d.ts +7 -109
  345. package/dist/upward.js +3 -0
  346. package/dist/usage-guide.js +3 -1
  347. package/dist/validate.d.ts +4 -2
  348. package/dist/validate.js +86 -9
  349. package/dist/version.d.ts +1 -1
  350. package/dist/version.js +1 -1
  351. package/dist/worker.js +312 -292
  352. package/dist/workspace-subject.js +5 -1
  353. package/dist/write-outcome.js +454 -34
  354. package/documentation/README.md +27 -7
  355. package/documentation/architecture.md +2547 -226
  356. package/documentation/auth.md +821 -98
  357. package/documentation/code-mode.md +49 -16
  358. package/documentation/deploying.md +164 -20
  359. package/documentation/hosted-mcp-migration.md +460 -0
  360. package/documentation/integrating.md +129 -144
  361. package/documentation/meta-tools.md +409 -32
  362. package/documentation/operating.md +27 -3
  363. package/documentation/operator-tests.md +3 -0
  364. package/documentation/operator-ui.md +68 -45
  365. package/documentation/provider-auth-migration.md +16 -63
  366. package/documentation/provider-migration-0.29.md +10 -5
  367. package/documentation/provider-migration-0.35.md +975 -0
  368. package/documentation/providers.md +965 -0
  369. package/examples/worker/AGENTS.md +23 -9
  370. package/examples/worker/README.md +254 -135
  371. package/examples/worker/alchemy.run.ts +116 -0
  372. package/examples/worker/gitignore +10 -0
  373. package/examples/worker/src/connecta.config.ts +23 -21
  374. package/examples/worker/wrangler.jsonc +14 -5
  375. package/guides/connecting-agents.md +157 -0
  376. package/guides/how-it-works.md +156 -0
  377. package/guides/safety-and-trust.md +153 -0
  378. package/package.json +27 -46
  379. package/templates/node/.env.example +3 -0
  380. package/templates/node/README.md +22 -2
  381. package/templates/node/docker-compose.yml +1 -0
  382. package/templates/node/package.json +3 -2
  383. package/templates/node/src/connecta.config.ts +8 -0
  384. package/dist/catalog-drift.d.ts +0 -138
  385. package/dist/catalog-drift.js +0 -578
  386. package/dist/providers/_shared/request-token.js +0 -83
  387. package/dist/providers/_shared/rest/operation-index.d.ts +0 -133
  388. package/dist/providers/_shared/rest/tools.d.ts +0 -141
  389. package/dist/providers/_shared/rest/value-safety.d.ts +0 -201
  390. package/dist/providers/basecamp/index.d.ts +0 -45
  391. package/dist/providers/basecamp/index.js +0 -997
  392. package/dist/providers/basecamp/skill.generated.js +0 -9
  393. package/dist/providers/cloudflare/rest.d.ts +0 -37
  394. package/dist/providers/cloudflare/rest.js +0 -1097
  395. package/dist/providers/github/hosted.js +0 -218
  396. package/dist/providers/github/rest.js +0 -321
  397. package/dist/providers/infisical/index.d.ts +0 -7
  398. package/dist/providers/infisical/index.js +0 -713
  399. package/dist/providers/infisical/skill.generated.js +0 -8
  400. package/dist/providers/intercom/index.d.ts +0 -24
  401. package/dist/providers/intercom/index.js +0 -121
  402. package/dist/providers/intercom/skill.generated.js +0 -8
  403. package/dist/providers/linear/index.d.ts +0 -55
  404. package/dist/providers/linear/index.js +0 -537
  405. package/dist/providers/linear/skill.generated.js +0 -8
  406. package/dist/providers/mixpanel/index.d.ts +0 -59
  407. package/dist/providers/mixpanel/index.js +0 -472
  408. package/dist/providers/mixpanel/operations.js +0 -20
  409. package/dist/providers/mixpanel/rest.js +0 -603
  410. package/dist/providers/mixpanel/skill.generated.js +0 -10
  411. package/dist/providers/mixpanel/value-safety.js +0 -16
  412. package/dist/providers/notion/index.d.ts +0 -67
  413. package/dist/providers/notion/index.js +0 -368
  414. package/dist/providers/notion/openapi.generated.js +0 -75
  415. package/dist/providers/notion/rest.d.ts +0 -24
  416. package/dist/providers/notion/rest.js +0 -1226
  417. package/dist/providers/notion/skill.generated.js +0 -11
  418. package/dist/providers/notion/value-safety.js +0 -56
  419. package/dist/providers/revenuecat/index.d.ts +0 -51
  420. package/dist/providers/revenuecat/index.js +0 -697
  421. package/dist/providers/revenuecat/skill.generated.js +0 -11
  422. package/dist/providers/stripe/rest.d.ts +0 -14
  423. package/dist/providers/stripe/rest.js +0 -402
  424. package/dist/providers/vercel/rest.d.ts +0 -20
  425. package/dist/providers/vercel/rest.js +0 -1020
  426. package/dist/providers/wispr-flow/index.d.ts +0 -14
  427. package/dist/providers/wispr-flow/index.js +0 -117
  428. package/dist/providers/wispr-flow/skill.generated.js +0 -8
  429. /package/dist/providers/github/{key.js → lib/key.js} +0 -0
@@ -1,15 +1,72 @@
1
1
  import { UnauthorizedError } from "@modelcontextprotocol/client";
2
2
  import { Deferred, Effect } from "effect";
3
- import { revokeDownstreamGrant } from "./downstream-revocation.js";
4
- import { authenticateRemoteClient, downstreamClientMetadata, remoteClientAuthMethod, } from "./downstream-client-metadata.js";
3
+ import { byteReadResponse } from "../byte-read-response.js";
4
+ import { deriveContext } from "../derived-context.js";
5
+ import { ConnectorCallError, resendAdvice } from "../errors.js";
6
+ import { oauthPartitionFor, retainOAuthPartition } from "../oauth-partition.js";
7
+ import { keepErrorName } from "../redaction-identity.js";
8
+ import { fromSignal, runEdge } from "../runtime/run.js";
5
9
  import { SentSecrets } from "../sent-secrets.js";
6
- import { ConnectorCallError } from "../errors.js";
7
- import { attachOAuthPartition, oauthPartitionFor, retainOAuthPartition } from "../oauth-partition.js";
8
- import { inheritOAuthSealer } from "../oauth-sealing.js";
9
- import { detach, fromSignal, runEdge } from "../runtime/run.js";
10
- import { OAUTH_FLOW_TTL_SECONDS, OAUTH_REFRESH_LEASE_SECONDS, oauthFlowKeys, oauthGrantKeys, oauthRefreshKeys, oauthRefreshSpentKeys, oauthRefreshActiveKeys, } from "../storage/keys.js";
11
- import { isRefreshTokenRequest, isOAuthCredentialRequest, refreshResponseOutcome, sdkSafeTokenResponse, tokenGrantType, } from "./oauth-token-response.js";
12
- import { deleteV2Keys, discoveryIssuer, readV2Grant } from "./oauth-v2-migration.js";
10
+ import { OAUTH_FLOW_TTL_SECONDS, OAUTH_REFRESH_LEASE_SECONDS, oauthFlowKeys, oauthGrantKeys, oauthRefreshActiveKeys, oauthRefreshKeys, oauthRefreshSpentKeys, } from "../storage/keys.js";
11
+ import { authenticateRemoteClient, downstreamClientMetadata, remoteClientAuthMethod, } from "./downstream-client-metadata.js";
12
+ import { revokeDownstreamGrant } from "./downstream-revocation.js";
13
+ import { authAwait, authCall, authCatch, authDetached, authFlow, authInvoke, authMethod, authOwnMethod, authProgram, authStorage, authThen, authValue, registerAuth, registerAuthMethod, } from "./native.js";
14
+ import { isOAuthCredentialRequest, isRefreshTokenRequest, refreshResponseOutcome, sdkSafeTokenResponse, tokenGrantType, } from "./oauth-token-response.js";
15
+ import { deleteV2Keys, discoveryIssuer, readV2Grant, } from "./oauth-v2-migration.js";
16
+ /** @internal Keep the SDK's cloned body reads inside the code exchange's request. */
17
+ export function codeExchangeResponse(response, signal) {
18
+ if (!response.body)
19
+ return response;
20
+ const reader = response.body.getReader();
21
+ void reader.closed.catch(() => { });
22
+ let closed = false;
23
+ let abort;
24
+ const release = () => {
25
+ closed = true;
26
+ signal.removeEventListener("abort", abort);
27
+ reader.releaseLock();
28
+ };
29
+ const body = new ReadableStream({
30
+ start(controller) {
31
+ abort = () => {
32
+ if (closed)
33
+ return;
34
+ void reader.cancel().catch(() => { });
35
+ release();
36
+ controller.error(signal.reason);
37
+ };
38
+ signal.addEventListener("abort", abort, { once: true });
39
+ if (signal.aborted)
40
+ abort();
41
+ },
42
+ async pull(controller) {
43
+ try {
44
+ const next = await reader.read();
45
+ if (closed)
46
+ return;
47
+ if (next.done) {
48
+ release();
49
+ controller.close();
50
+ }
51
+ else
52
+ controller.enqueue(next.value);
53
+ }
54
+ catch (error) {
55
+ if (!closed) {
56
+ release();
57
+ controller.error(error);
58
+ }
59
+ }
60
+ },
61
+ cancel() {
62
+ if (!closed) {
63
+ void reader.cancel().catch(() => { });
64
+ release();
65
+ }
66
+ },
67
+ });
68
+ return byteReadResponse(new Response(body, { status: response.status, statusText: response.statusText, headers: response.headers }));
69
+ }
13
70
  /** RFC 6749 section 3.3: one or more scope tokens, separated by single spaces. */
14
71
  const OAUTH_SCOPE = /^[\x21\x23-\x5B\x5D-\x7E]+(?: [\x21\x23-\x5B\x5D-\x7E]+)*$/;
15
72
  /** Refuse, at construction, a configured scope no authorization server could parse. */
@@ -20,16 +77,13 @@ export function assertOAuthScope(connectorId, scope) {
20
77
  }
21
78
  /**
22
79
  * The context an explicit authorization start runs under: allowed to begin
23
- * consent, with a request scope of its own, carrying the registry's sealer and
24
- * owner partition across the copy. Only `startAuth` builds one; status reads
25
- * and calls never do, which is what keeps them from starting authorization.
80
+ * consent, in the same request scope, and derived so the registry's sealer,
81
+ * owner partition, and every other call-state entry carry across the copy.
82
+ * Only `startAuth` builds one; status reads and calls never do, which is what
83
+ * keeps them from starting authorization.
26
84
  */
27
85
  export function authorizingContext(ctx) {
28
- return attachOAuthPartition(inheritOAuthSealer(ctx, {
29
- ...ctx,
30
- requestScope: ctx.requestScope ?? ctx,
31
- allowAuthorization: true,
32
- }), oauthPartitionFor(ctx));
86
+ return deriveContext(ctx, { allowAuthorization: true });
33
87
  }
34
88
  /**
35
89
  * A callback whose consent another callback already claimed: fixed text, and
@@ -42,6 +96,7 @@ export class OAuthCallbackClaimedError extends ConnectorCallError {
42
96
  this.name = "OAuthCallbackClaimedError";
43
97
  }
44
98
  }
99
+ keepErrorName(OAuthCallbackClaimedError, "OAuthCallbackClaimedError");
45
100
  /** A local destination guard proved that no OAuth HTTP request left. */
46
101
  export class OAuthRequestNotSentError extends ConnectorCallError {
47
102
  }
@@ -73,18 +128,51 @@ const REFRESH_REQUEST_DEADLINE_MS = 20_000;
73
128
  const REFRESH_WAIT_DEADLINE_MS = 35_000;
74
129
  function refreshWait(signal) {
75
130
  const deadline = new AbortController();
76
- const timer = setTimeout(() => deadline.abort(), REFRESH_WAIT_DEADLINE_MS);
131
+ let timer = setTimeout(() => deadline.abort(), REFRESH_WAIT_DEADLINE_MS);
77
132
  let closed = false;
133
+ let transferred = false;
134
+ let requestStarted = false;
78
135
  const abort = () => deadline.abort(aborted(signal));
79
136
  signal?.addEventListener("abort", abort, { once: true });
80
137
  if (signal?.aborted)
81
138
  abort();
82
139
  return {
83
140
  signal: deadline.signal,
84
- run: (operation) => runEdge(Effect.raceAllFirst([
85
- fromSignal(deadline.signal),
86
- Effect.tryPromise({ try: operation, catch: (error) => error }),
87
- ])),
141
+ get requestStarted() {
142
+ return requestStarted;
143
+ },
144
+ run: authMethod((operation) => {
145
+ return authFlow(function* () {
146
+ // A race's losing operation may still start. Check before constructing it,
147
+ // then again before handing a late result back to preparation.
148
+ if (deadline.signal.aborted)
149
+ return (yield Effect.fail(aborted(deadline.signal)));
150
+ return (yield authThen(Effect.raceAllFirst([fromSignal(deadline.signal), authCall(operation, [])]), (value) => {
151
+ if (deadline.signal.aborted)
152
+ throw aborted(deadline.signal);
153
+ return value;
154
+ }));
155
+ });
156
+ }),
157
+ transfer: () => {
158
+ if (deadline.signal.aborted)
159
+ throw aborted(deadline.signal);
160
+ if (transferred)
161
+ return;
162
+ // Synchronous with CAS submission: cancellation can prevent the write
163
+ // only before this point. Keep the independent preparation timer alive.
164
+ signal?.removeEventListener("abort", abort);
165
+ transferred = true;
166
+ },
167
+ beginRequest: () => {
168
+ if (deadline.signal.aborted)
169
+ throw aborted(deadline.signal);
170
+ requestStarted = true;
171
+ clearTimeout(timer);
172
+ timer = setTimeout(() => {
173
+ deadline.abort(new DOMException("OAuth refresh timed out", "TimeoutError"));
174
+ }, REFRESH_REQUEST_DEADLINE_MS);
175
+ },
88
176
  close: () => {
89
177
  if (closed)
90
178
  return;
@@ -123,9 +211,16 @@ function tokenResponse(tokens) {
123
211
  return Response.json(answer);
124
212
  }
125
213
  function refreshTokensDigest(tokens) {
126
- const { issuer: _issuer, ...answer } = tokens;
127
- return oauthStateDigest(JSON.stringify(Object.fromEntries(Object.entries(answer).sort(([a], [b]) => a.localeCompare(b)))));
214
+ return runEdge(authProgram(refreshTokensDigestNative, [tokens]));
128
215
  }
216
+ const refreshTokensDigestNative = /* @__PURE__ */ registerAuth(refreshTokensDigest, function (tokens) {
217
+ return authFlow(function* () {
218
+ const { issuer: _issuer, ...answer } = tokens;
219
+ return (yield authCall(oauthStateDigest, [
220
+ JSON.stringify(Object.fromEntries(Object.entries(answer).sort(([a], [b]) => a.localeCompare(b)))),
221
+ ]));
222
+ });
223
+ });
129
224
  function requestedRefreshToken(init) {
130
225
  return init?.body instanceof URLSearchParams
131
226
  ? init.body.get("refresh_token")
@@ -154,254 +249,91 @@ export class OAuthRefreshCoordinator {
154
249
  constructor(retainPartition = () => () => { }) {
155
250
  this.retainPartition = retainPartition;
156
251
  }
157
- coordinatedFetch(provider, baseFetch, requestSignal, defer, resourceFetch = baseFetch) {
158
- return async (input, init) => {
159
- // Freeze the form before any await: classification and the spent digest
160
- // must describe the exact token sent, even if its caller mutates a form.
161
- init = init
162
- ? { ...init, ...(init.body instanceof URLSearchParams ? { body: new URLSearchParams(init.body) } : {}) }
163
- : undefined;
164
- if (isRefreshTokenRequest(init)) {
165
- const signal = requestSignal && init?.signal
166
- ? AbortSignal.any([requestSignal, init.signal])
167
- : (requestSignal ?? init?.signal ?? undefined);
168
- return this.refresh(provider, input, init, baseFetch, signal, defer);
169
- }
170
- const grantType = tokenGrantType(init);
171
- const tokenRequest = isOAuthCredentialRequest(init);
172
- const exchange = grantType === "authorization_code";
173
- if (exchange) {
174
- const answered = await provider.claimCodeExchange();
175
- if (answered)
176
- return answered;
177
- }
178
- const dispatch = () => {
179
- if (requestSignal?.aborted)
180
- throw aborted(requestSignal);
181
- const signal = requestSignal
182
- ? init?.signal
252
+ coordinatedFetch(provider, baseFetch, requestSignal, defer, resourceFetch = baseFetch, prepareCredentialRequest) {
253
+ return authMethod((input, init) => {
254
+ return authFlow(function* () {
255
+ // Freeze the form before any await: classification and the spent digest
256
+ // must describe the exact token sent, even if its caller mutates a form.
257
+ init = init
258
+ ? { ...init, ...(init.body instanceof URLSearchParams ? { body: new URLSearchParams(init.body) } : {}) }
259
+ : undefined;
260
+ // Production adapters resolve the required host policy before a code or
261
+ // refresh claim can consume its single-use credential. Custom SDK leaves
262
+ // may supply their own policy; this hook performs no I/O and owns no data.
263
+ if (isOAuthCredentialRequest(init))
264
+ prepareCredentialRequest?.();
265
+ if (isRefreshTokenRequest(init)) {
266
+ const signal = requestSignal && init?.signal
183
267
  ? AbortSignal.any([requestSignal, init.signal])
184
- : requestSignal
185
- : init?.signal;
186
- // Credential-bearing OAuth requests use the raw send path. A redirect
187
- // is a dispatched failure, never authority to send the form again.
188
- return tokenRequest
189
- ? baseFetch(input, { ...init, redirect: "manual", ...(signal ? { signal } : {}) })
190
- : resourceFetch(input, { ...init, ...(signal ? { signal } : {}) });
191
- };
192
- // Awaited here so workerd associates a rejection with the fetch the SDK
193
- // is already awaiting, not with an adopted inner promise.
194
- const response = await (exchange ? provider.dispatchCodeExchange(dispatch, input) : dispatch());
195
- if (!tokenRequest)
196
- return response;
197
- // A code exchange's failure is the SDK's to log as well.
198
- const forSdk = await sdkSafeTokenResponse(response, exchange ? (tokens) => provider.recordCodeExchangeTokens(tokens) : undefined);
199
- if (exchange && forSdk !== response)
200
- provider.recordCodeExchangeRefusal(forSdk);
201
- return forSdk;
202
- };
203
- }
204
- async refresh(provider, input, init, baseFetch, signal, defer) {
205
- const waiting = refreshWait(signal);
206
- try {
207
- // A verdict belongs to one refresh; never let an older one decide this.
208
- provider.recordRefreshFailure(undefined);
209
- const epoch = await waiting.run(() => provider.flowEpoch());
210
- const requested = requestedRefreshToken(init);
211
- for (let attempt = 0; attempt < 8; attempt++) {
212
- if (signal?.aborted)
213
- throw aborted(signal);
214
- const joined = this.flights.get(epoch);
215
- if (joined) {
216
- const outcome = await waitForRefreshFlight(joined, signal, waiting.signal);
217
- if (outcome.status === "failed") {
218
- provider.recordRefreshFailure(outcome.verdict);
219
- throw outcome.error;
220
- }
221
- if (outcome.status === "refreshed")
222
- return provider.adoptRefresh(outcome.tokens);
223
- continue;
224
- }
225
- // Preparation reads share the same bound as later claim waits.
226
- const current = await waiting.run(() => provider.storedTokens());
227
- if (this.flights.has(epoch))
228
- continue;
229
- if (current?.refresh_token &&
230
- (current.refresh_token !== requested || provider.refreshedSinceRead(current, epoch))) {
231
- return provider.adoptRefresh(current);
232
- }
233
- if (current?.refresh_token !== requested) {
234
- provider.recordRefreshFailure({ kind: "dead" });
235
- return Response.json({ error: "invalid_grant", error_description: "Refresh token is no longer active." }, { status: 400 });
236
- }
237
- const redemption = this.redeem(provider, epoch, requested, input, init, baseFetch, signal, waiting);
238
- // The refresh owns its completion after dispatch, including storage.
239
- // Attach it before yielding so a cancelled Worker keeps that work alive.
240
- try {
241
- detach(Effect.promise(() => redemption), defer ? { waitUntil: defer } : undefined);
268
+ : (requestSignal ?? init?.signal ?? undefined);
269
+ return (yield authInvoke(this, "refresh", [provider, input, init, baseFetch, signal, defer]));
242
270
  }
243
- catch {
244
- // A missing or refusing runtime hook still leaves the promise running.
271
+ const grantType = tokenGrantType(init);
272
+ const tokenRequest = isOAuthCredentialRequest(init);
273
+ const exchange = grantType === "authorization_code";
274
+ if (exchange) {
275
+ const answered = (yield authInvoke(provider, "claimCodeExchange", []));
276
+ if (answered)
277
+ return answered;
245
278
  }
246
- if (!signal)
247
- return await redemption;
248
- return await runEdge(Effect.raceAllFirst([
249
- Effect.tryPromise({ try: () => redemption, catch: (error) => error }),
250
- fromSignal(signal),
279
+ const dispatch = authMethod(() => {
280
+ return authFlow(function* () {
281
+ if (requestSignal?.aborted)
282
+ throw aborted(requestSignal);
283
+ const signal = requestSignal
284
+ ? init?.signal
285
+ ? AbortSignal.any([requestSignal, init.signal])
286
+ : requestSignal
287
+ : init?.signal;
288
+ // Credential-bearing OAuth requests use the raw send path. A redirect
289
+ // is a dispatched failure, never authority to send the form again.
290
+ return (yield tokenRequest
291
+ ? authCall(baseFetch, [input, { ...init, redirect: "manual", ...(signal ? { signal } : {}) }])
292
+ : authCall(resourceFetch, [input, { ...init, ...(signal ? { signal } : {}) }]));
293
+ }.bind(this));
294
+ });
295
+ // Await the composed fetch here so workerd associates a rejection with
296
+ // the fetch the SDK is already awaiting.
297
+ const response = (yield exchange
298
+ ? authInvoke(provider, "dispatchCodeExchange", [dispatch, input])
299
+ : authCall(dispatch, []));
300
+ if (!tokenRequest)
301
+ return response;
302
+ // A code exchange's failure is the SDK's to log as well.
303
+ const forSdk = (yield authCall(sdkSafeTokenResponse, [
304
+ response,
305
+ exchange
306
+ ? authMethod((tokens) => {
307
+ return authInvoke(provider, "recordCodeExchangeTokens", [tokens]);
308
+ })
309
+ : undefined,
251
310
  ]));
252
- }
253
- throw provider.refreshContended();
254
- }
255
- catch (error) {
256
- if (signal?.aborted)
257
- throw aborted(signal);
258
- if (waiting.signal.aborted)
259
- throw provider.refreshContended();
260
- throw error;
261
- }
262
- finally {
263
- waiting.close();
264
- }
311
+ if (exchange && forSdk !== response)
312
+ provider.recordCodeExchangeRefusal(forSdk);
313
+ return forSdk;
314
+ }.bind(this));
315
+ });
265
316
  }
266
- async redeem(provider, epoch, requested, input, init, baseFetch, signal, waiting) {
267
- const flight = { outcome: Deferred.makeUnsafe() };
268
- this.flights.set(epoch, flight);
269
- // Pin the owner partition until the flight settles, whoever is waiting.
270
- const release = this.retainPartition();
271
- const settle = (outcome) => {
272
- if (this.flights.get(epoch) === flight)
273
- this.flights.delete(epoch);
274
- release();
275
- // Last, because joined fibers resume inside this call.
276
- Deferred.doneUnsafe(flight.outcome, Effect.succeed(outcome));
277
- };
278
- const fail = (error, verdict) => {
279
- if (verdict)
280
- provider.recordRefreshFailure(verdict);
281
- settle({ status: "failed", error, ...(verdict ? { verdict } : {}) });
282
- };
283
- // The authorization server this redemption answers to, fixed now.
284
- const issuer = provider.refreshIssuer(epoch);
285
- let lease;
286
- try {
287
- const claim = await provider.claimRefresh(epoch, requested, signal, waiting);
288
- if (claim instanceof Response) {
289
- const tokens = await waiting.run(() => provider.storedTokens());
290
- if (tokens)
291
- settle({ status: "refreshed", tokens });
292
- else
293
- settle({ status: "retired" });
294
- return claim;
295
- }
296
- lease = claim;
297
- }
298
- catch (error) {
299
- fail(error, provider.refreshVerdict());
300
- throw error;
301
- }
302
- const releaseLease = async (verdict, resolution = "ambiguous") => {
303
- await provider.releaseRefresh(lease, verdict, undefined, resolution);
304
- };
305
- let outcome;
306
- const deadline = new AbortController();
307
- let dispatched = false;
308
- let timer;
309
- try {
310
- const read = provider
311
- .dispatchRefresh(lease, () => {
312
- if (signal?.aborted)
313
- throw aborted(signal);
314
- if (init?.signal?.aborted)
315
- throw aborted(init.signal);
316
- dispatched = true;
317
- // Preparation remains bounded through the send transition. After
318
- // dispatch, caller cancellation must not prevent an answer's commit.
319
- waiting.close();
320
- // Start the deadline at dispatch, including the response body read.
321
- timer = setTimeout(() => {
322
- deadline.abort(new DOMException("OAuth refresh timed out", "TimeoutError"));
323
- }, REFRESH_REQUEST_DEADLINE_MS);
324
- return baseFetch(input, { ...init, redirect: "manual", signal: deadline.signal });
325
- }, waiting)
326
- .then((response) => {
327
- if (deadline.signal.aborted) {
328
- void response.body?.cancel().catch(() => { });
329
- throw aborted(deadline.signal);
330
- }
331
- return refreshResponseOutcome(response, deadline.signal);
332
- });
333
- // The deadline can settle the race before a late fetch/body rejects.
334
- void read.catch(() => { });
335
- outcome = await runEdge(Effect.raceAllFirst([
336
- Effect.tryPromise({ try: () => read, catch: (error) => error }),
337
- fromSignal(deadline.signal),
338
- ]));
339
- if (deadline.signal.aborted)
340
- throw aborted(deadline.signal);
341
- }
342
- catch (error) {
343
- if (!dispatched) {
344
- // A cancelled or timed-out unsent holder leaves a safely expiring
345
- // claim. Cleanup must not retain its local flight or partition pin.
346
- if (!waiting.signal.aborted && !signal?.aborted && !init?.signal?.aborted) {
347
- await waiting.run(() => releaseLease()).catch(() => { });
348
- if (provider.refreshVerdict()?.kind === "dead") {
349
- await waiting.run(() => provider.discardRefusedGrant(requested, epoch)).catch(() => { });
350
- }
351
- }
352
- const failure = !signal?.aborted && waiting.signal.aborted ? provider.refreshContended() : error;
353
- fail(failure, provider.refreshVerdict());
354
- throw failure;
355
- }
356
- // A sent request without a definitive answer may have rotated the token.
357
- // Persist the refusal before dropping tokens, so interrupted cleanup or a
358
- // later write of the same fingerprint cannot make it redeemable again.
359
- const failure = new UnauthorizedError("OAuth refresh outcome is unknown; authorization required.");
360
- await releaseLease({ kind: "dead" });
361
- await provider.discardRefusedGrant(requested, epoch);
362
- fail(failure, { kind: "dead" });
363
- throw failure;
364
- }
365
- finally {
366
- if (timer !== undefined)
367
- clearTimeout(timer);
368
- }
369
- // An answer exists from here on, and is committed whatever the owner does.
370
- if (outcome.failure) {
371
- // Drop a refused grant while the flight still stands: a caller arriving
372
- // meanwhile joins it instead of redeeming the dead token again.
373
- await provider.discardRefusedGrant(requested, epoch);
374
- await releaseLease(outcome.verdict, "resolved");
375
- fail(outcome.failure, outcome.verdict);
376
- return outcome.forSdk;
377
- }
378
- // The server consumed the old refresh token; the SDK merges it back the
379
- // same way when a rotation omits one.
380
- const accepted = {
381
- ...outcome.tokens,
382
- ...(outcome.tokens.refresh_token === undefined && requested !== null ? { refresh_token: requested } : {}),
383
- };
384
- let committed;
385
- try {
386
- committed = await provider.storeRefresh(epoch, requested, accepted, issuer);
387
- }
388
- catch {
389
- const failure = new UnauthorizedError("OAuth refresh could not be committed; authorization required.");
390
- await releaseLease({ kind: "dead" }, "resolved");
391
- await provider.discardRefusedGrant(requested, epoch);
392
- fail(failure, { kind: "dead" });
393
- throw failure;
394
- }
395
- if (!committed) {
396
- // Restart, Disconnect, or another consent decides the newer grant.
397
- await releaseLease({ kind: "dead" }, "resolved");
398
- const failure = new UnauthorizedError("OAuth refresh grant is no longer active.");
399
- fail(failure, { kind: "dead" });
400
- throw failure;
401
- }
402
- await provider.releaseRefresh(lease, undefined, committed, "resolved");
403
- settle({ status: "refreshed", tokens: committed });
404
- return provider.adoptRefresh(committed);
317
+ refresh(provider, input, init, baseFetch, signal, defer) {
318
+ return runEdge(authOwnMethod(OAuthRefreshCoordinator.prototype, this, "refresh", [
319
+ provider,
320
+ input,
321
+ init,
322
+ baseFetch,
323
+ signal,
324
+ defer,
325
+ ]));
326
+ }
327
+ redeem(provider, epoch, requested, input, init, baseFetch, waiting) {
328
+ return runEdge(authOwnMethod(OAuthRefreshCoordinator.prototype, this, "redeem", [
329
+ provider,
330
+ epoch,
331
+ requested,
332
+ input,
333
+ init,
334
+ baseFetch,
335
+ waiting,
336
+ ]));
405
337
  }
406
338
  /** A reset fenced `epoch`: wake its joiners now rather than at the answer. */
407
339
  retire(epoch) {
@@ -411,6 +343,235 @@ export class OAuthRefreshCoordinator {
411
343
  this.flights.delete(epoch);
412
344
  Deferred.doneUnsafe(flight.outcome, Effect.succeed({ status: "retired" }));
413
345
  }
346
+ static {
347
+ registerAuthMethod(OAuthRefreshCoordinator.prototype, "refresh", function (provider, input, init, baseFetch, signal, defer) {
348
+ return authFlow(function* () {
349
+ if (signal?.aborted)
350
+ throw aborted(signal);
351
+ const waiting = refreshWait(signal);
352
+ let redeeming = false;
353
+ try {
354
+ // A verdict belongs to one refresh; never let an older one decide this.
355
+ provider.recordRefreshFailure(undefined);
356
+ const epoch = (yield authInvoke(waiting, "run", [
357
+ authMethod(() => {
358
+ return authInvoke(provider, "flowEpoch", []);
359
+ }),
360
+ ]));
361
+ const requested = requestedRefreshToken(init);
362
+ for (let attempt = 0; attempt < 8; attempt++) {
363
+ if (signal?.aborted)
364
+ throw aborted(signal);
365
+ const joined = this.flights.get(epoch);
366
+ if (joined) {
367
+ const outcome = (yield authCall(waitForRefreshFlight, [
368
+ joined,
369
+ signal,
370
+ waiting.signal,
371
+ ]));
372
+ if (signal?.aborted)
373
+ throw aborted(signal);
374
+ if (outcome.status === "failed") {
375
+ provider.recordRefreshFailure(outcome.verdict);
376
+ throw outcome.error;
377
+ }
378
+ if (outcome.status === "refreshed")
379
+ return provider.adoptRefresh(outcome.tokens);
380
+ continue;
381
+ }
382
+ // Preparation reads share the same bound as later claim waits.
383
+ const current = (yield authInvoke(waiting, "run", [
384
+ authMethod(() => {
385
+ return authInvoke(provider, "storedTokens", [waiting.signal]);
386
+ }),
387
+ ]));
388
+ if (this.flights.has(epoch))
389
+ continue;
390
+ if (current?.refresh_token &&
391
+ (current.refresh_token !== requested || provider.refreshedSinceRead(current, epoch))) {
392
+ return provider.adoptRefresh(current);
393
+ }
394
+ if (current?.refresh_token !== requested) {
395
+ provider.recordRefreshFailure({ kind: "dead" });
396
+ return Response.json({ error: "invalid_grant", error_description: "Refresh token is no longer active." }, { status: 400 });
397
+ }
398
+ redeeming = true;
399
+ // Claim submission transfers renewal, including preparation, token
400
+ // request and commit, to a bounded native coordinator. Only
401
+ // this caller's wait races its signal; the coordinator receives
402
+ // no request/call Context authority or caller-owned deadline.
403
+ const redemption = authDetached(authInvoke(this, "redeem", [
404
+ provider,
405
+ epoch,
406
+ requested,
407
+ input,
408
+ init,
409
+ baseFetch,
410
+ waiting,
411
+ ]).pipe(Effect.ensuring(Effect.sync(() => waiting.close()))), defer);
412
+ return (yield signal ? Effect.raceFirst(redemption, fromSignal(signal)) : redemption);
413
+ }
414
+ throw provider.refreshContended();
415
+ }
416
+ catch (error) {
417
+ if (signal?.aborted)
418
+ throw aborted(signal);
419
+ if (waiting.signal.aborted && !waiting.requestStarted)
420
+ throw provider.refreshContended();
421
+ throw error;
422
+ }
423
+ finally {
424
+ if (!redeeming)
425
+ waiting.close();
426
+ }
427
+ }.bind(this));
428
+ }, OAuthRefreshCoordinator.prototype.refresh);
429
+ registerAuthMethod(OAuthRefreshCoordinator.prototype, "redeem", function (provider, epoch, requested, input, init, baseFetch, waiting) {
430
+ return authFlow(function* () {
431
+ const flight = { outcome: Deferred.makeUnsafe() };
432
+ this.flights.set(epoch, flight);
433
+ // Pin the owner partition until the flight settles, whoever is waiting.
434
+ const release = this.retainPartition();
435
+ const settle = (outcome) => {
436
+ if (this.flights.get(epoch) === flight)
437
+ this.flights.delete(epoch);
438
+ release();
439
+ // Last, because joined fibers resume inside this call.
440
+ Deferred.doneUnsafe(flight.outcome, Effect.succeed(outcome));
441
+ };
442
+ const fail = (error, verdict) => {
443
+ if (verdict)
444
+ provider.recordRefreshFailure(verdict);
445
+ settle({ status: "failed", error, ...(verdict ? { verdict } : {}) });
446
+ };
447
+ // The authorization server this redemption answers to, fixed now.
448
+ const issuer = provider.refreshIssuer(epoch);
449
+ let lease;
450
+ try {
451
+ const claim = (yield authInvoke(provider, "claimRefresh", [epoch, requested, undefined, waiting]));
452
+ if (claim instanceof Response) {
453
+ const tokens = (yield authInvoke(waiting, "run", [
454
+ authMethod(() => {
455
+ return authInvoke(provider, "storedTokens", [waiting.signal]);
456
+ }),
457
+ ]));
458
+ if (tokens)
459
+ settle({ status: "refreshed", tokens });
460
+ else
461
+ settle({ status: "retired" });
462
+ return claim;
463
+ }
464
+ lease = claim;
465
+ }
466
+ catch (error) {
467
+ fail(error, provider.refreshVerdict());
468
+ throw error;
469
+ }
470
+ const releaseLease = authMethod((verdict, resolution = "ambiguous") => {
471
+ return authFlow(function* () {
472
+ (yield authInvoke(provider, "releaseRefresh", [lease, verdict, undefined, resolution]));
473
+ }.bind(this));
474
+ });
475
+ let outcome;
476
+ const deadline = waiting.signal;
477
+ let dispatched = false;
478
+ try {
479
+ const read = authFlow(function* () {
480
+ const response = (yield authInvoke(provider, "dispatchRefresh", [
481
+ lease,
482
+ authMethod(() => authFlow(function* () {
483
+ waiting.beginRequest();
484
+ dispatched = true;
485
+ return (yield authCall(baseFetch, [
486
+ input,
487
+ { ...init, redirect: "manual", signal: deadline },
488
+ ]));
489
+ }.bind(this))),
490
+ waiting,
491
+ ]));
492
+ if (deadline.aborted) {
493
+ void response.body?.cancel().catch(() => { });
494
+ throw aborted(deadline);
495
+ }
496
+ return (yield authCall(refreshResponseOutcome, [response, deadline]));
497
+ }.bind(this));
498
+ outcome = (yield Effect.raceFirst(read, fromSignal(deadline)));
499
+ if (deadline.aborted)
500
+ throw aborted(deadline);
501
+ }
502
+ catch (error) {
503
+ if (!dispatched) {
504
+ // Only the coordinator's own preparation deadline can interrupt a
505
+ // submitted claim. Caller cancellation no longer reaches this work.
506
+ if (!waiting.signal.aborted) {
507
+ (yield authCatch(authInvoke(waiting, "run", [
508
+ authMethod(() => {
509
+ return authCall(releaseLease, []);
510
+ }),
511
+ ]), () => { }));
512
+ if (provider.refreshVerdict()?.kind === "dead") {
513
+ (yield authCatch(authInvoke(waiting, "run", [
514
+ authMethod(() => {
515
+ return authInvoke(provider, "discardRefusedGrant", [requested, epoch]);
516
+ }),
517
+ ]), () => { }));
518
+ }
519
+ }
520
+ const failure = waiting.signal.aborted ? provider.refreshContended() : error;
521
+ fail(failure, provider.refreshVerdict());
522
+ throw failure;
523
+ }
524
+ // A sent request without a definitive answer may have rotated the token.
525
+ // Persist the refusal before dropping tokens, so interrupted cleanup or a
526
+ // later write of the same fingerprint cannot make it redeemable again.
527
+ const failure = new UnauthorizedError("OAuth refresh outcome is unknown; authorization required.");
528
+ (yield authCall(releaseLease, [{ kind: "dead" }]));
529
+ (yield authInvoke(provider, "discardRefusedGrant", [requested, epoch]));
530
+ fail(failure, { kind: "dead" });
531
+ throw failure;
532
+ }
533
+ finally {
534
+ waiting.close();
535
+ }
536
+ // An answer exists from here on, and is committed whatever the owner does.
537
+ if (outcome.failure) {
538
+ // Drop a refused grant while the flight still stands: a caller arriving
539
+ // meanwhile joins it instead of redeeming the dead token again.
540
+ (yield authInvoke(provider, "discardRefusedGrant", [requested, epoch]));
541
+ (yield authCall(releaseLease, [outcome.verdict, "resolved"]));
542
+ fail(outcome.failure, outcome.verdict);
543
+ return outcome.forSdk;
544
+ }
545
+ // The server consumed the old refresh token; the SDK merges it back the
546
+ // same way when a rotation omits one.
547
+ const accepted = {
548
+ ...outcome.tokens,
549
+ ...(outcome.tokens.refresh_token === undefined && requested !== null ? { refresh_token: requested } : {}),
550
+ };
551
+ let committed;
552
+ try {
553
+ committed = (yield authInvoke(provider, "storeRefresh", [epoch, requested, accepted, issuer]));
554
+ }
555
+ catch {
556
+ const failure = new UnauthorizedError("OAuth refresh could not be committed; authorization required.");
557
+ (yield authCall(releaseLease, [{ kind: "dead" }, "resolved"]));
558
+ (yield authInvoke(provider, "discardRefusedGrant", [requested, epoch]));
559
+ fail(failure, { kind: "dead" });
560
+ throw failure;
561
+ }
562
+ if (!committed) {
563
+ // Restart, Disconnect, or another consent decides the newer grant.
564
+ (yield authCall(releaseLease, [{ kind: "dead" }, "resolved"]));
565
+ const failure = new UnauthorizedError("OAuth refresh grant is no longer active.");
566
+ fail(failure, { kind: "dead" });
567
+ throw failure;
568
+ }
569
+ (yield authInvoke(provider, "releaseRefresh", [lease, undefined, committed, "resolved"]));
570
+ settle({ status: "refreshed", tokens: committed });
571
+ return provider.adoptRefresh(committed);
572
+ }.bind(this));
573
+ }, OAuthRefreshCoordinator.prototype.redeem);
574
+ }
414
575
  }
415
576
  /**
416
577
  * One refresh coordinator per owner partition, for one connector runtime.
@@ -452,18 +613,23 @@ const MAX_GRANT_WRITES = 32;
452
613
  * flow record's fifteen minutes.
453
614
  */
454
615
  const PENDING_AUTHORIZATION_MAX_AGE_MS = 10 * 60 * 1000;
455
- async function discoveryBinding(state) {
456
- return state
457
- ? {
458
- issuer: discoveryIssuer(state),
459
- tokenEndpoint: state.authorizationServerMetadata?.token_endpoint,
460
- ...(state.authorizationServerMetadata?.authorization_response_iss_parameter_supported === true
461
- ? { issRequired: true }
462
- : {}),
463
- discovery: await oauthStateDigest(JSON.stringify(state)),
464
- }
465
- : {};
616
+ function discoveryBinding(state) {
617
+ return runEdge(authProgram(discoveryBindingNative, [state]));
466
618
  }
619
+ const discoveryBindingNative = /* @__PURE__ */ registerAuth(discoveryBinding, function (state) {
620
+ return authFlow(function* () {
621
+ return state
622
+ ? {
623
+ issuer: discoveryIssuer(state),
624
+ tokenEndpoint: state.authorizationServerMetadata?.token_endpoint,
625
+ ...(state.authorizationServerMetadata?.authorization_response_iss_parameter_supported === true
626
+ ? { issRequired: true }
627
+ : {}),
628
+ discovery: (yield authCall(oauthStateDigest, [JSON.stringify(state)])),
629
+ }
630
+ : {};
631
+ });
632
+ });
467
633
  function plainObject(value) {
468
634
  return value !== null && typeof value === "object" && !Array.isArray(value);
469
635
  }
@@ -589,7 +755,7 @@ export class KvOAuthProvider {
589
755
  constructor(connectorId, storage, redirectUri, refreshCoordinator, allowAuthorization = true,
590
756
  /** Present when the deployment's vault can seal; the grant is then ciphertext. */
591
757
  sealer,
592
- /** Request ownership for a start; a timed-out SDK hook may still resume. */
758
+ /** SDK hook ownership; renewal ignores it after claim submission. */
593
759
  signal,
594
760
  /** Stable connector configuration bound to dynamically registered clients. */
595
761
  clientBinding,
@@ -624,129 +790,83 @@ export class KvOAuthProvider {
624
790
  const client = this.clientOptions?.client;
625
791
  return downstreamClientMetadata(this.redirectUri, this.scope, this.clientOptions?.name, client ? remoteClientAuthMethod(client) : "none");
626
792
  }
793
+ /** A preparation await owns no continuation after its call/request closes.
794
+ * null explicitly selects coordinator/reset ownership, never the caller. */
795
+ wait(work, signal = this.signal) {
796
+ return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "wait", [work, signal]));
797
+ }
798
+ /** Legacy helpers must fence their inner reads/deletes too, not just our join. */
799
+ preparationStorage(signal) {
800
+ if (!signal)
801
+ return this.storage;
802
+ const storage = this.storage;
803
+ return {
804
+ ...storage,
805
+ get: authMethod((key) => {
806
+ return authInvoke(this, "wait", [
807
+ authMethod(() => {
808
+ return authStorage(storage, "get", [key]);
809
+ }),
810
+ signal,
811
+ ]);
812
+ }),
813
+ list: authMethod((prefix) => {
814
+ return authInvoke(this, "wait", [
815
+ authMethod(() => {
816
+ return authStorage(storage, "list", [prefix]);
817
+ }),
818
+ signal,
819
+ ]);
820
+ }),
821
+ delete: authMethod((key) => {
822
+ return authInvoke(this, "wait", [
823
+ authMethod(() => {
824
+ return authStorage(storage, "delete", [key]);
825
+ }),
826
+ signal,
827
+ ]);
828
+ }),
829
+ };
830
+ }
627
831
  // --- the grant record ---------------------------------------------------
628
832
  /** The grant's additional authenticated data: record and epoch. */
629
833
  grantLabel(epoch) {
630
834
  return `${GRANT}:${epoch}`;
631
835
  }
632
- async encodeGrant(grant) {
633
- this.assertCredentialFree({ issuer: grant.body.issuer, discovery: grant.body.discovery }, grant.body);
634
- const stored = {
635
- connectaOAuth: GRANT_VERSION,
636
- epoch: grant.epoch,
637
- ...(grant.cleanupPending ? { cleanupPending: true } : {}),
638
- ...(grant.flow !== undefined ? { flow: grant.flow } : {}),
639
- };
640
- if (Object.keys(grant.body).length > 0) {
641
- if (this.sealer) {
642
- stored.sealed = await this.sealer.seal(this.grantLabel(grant.epoch), JSON.stringify(grant.body));
643
- }
644
- else {
645
- stored.body = grant.body;
646
- }
647
- }
648
- return JSON.stringify(stored);
836
+ encodeGrant(grant, signal = this.signal) {
837
+ return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "encodeGrant", [grant, signal]));
649
838
  }
650
839
  /**
651
840
  * Open a stored grant. A body that does not open — tampered, under a
652
841
  * rotated key, moved from another connector, owner, or epoch, or sealed
653
842
  * with no sealer configured — reads as empty: authorization again.
654
843
  */
655
- async openGrant(raw) {
656
- if (raw === null)
657
- return { epoch: INITIAL_EPOCH, body: {} };
658
- const stored = storedGrant(raw);
659
- if (!stored)
660
- return { epoch: UNREADABLE_EPOCH, body: {} };
661
- let body = stored.body;
662
- if (stored.sealed !== undefined) {
663
- body = undefined;
664
- try {
665
- if (this.sealer)
666
- body = parsed(await this.sealer.open(this.grantLabel(stored.epoch), stored.sealed));
667
- }
668
- catch {
669
- this.sealer?.warn(`[connecta] connector "${this.connectorId}" has a sealed OAuth grant ` +
670
- "the configured vault cannot open; treating it as absent, so the " +
671
- "connector needs authorization again. A changed vault key or " +
672
- "tampered storage causes this.");
673
- }
674
- }
675
- return {
676
- epoch: stored.epoch,
677
- ...(stored.cleanupPending ? { cleanupPending: true } : {}),
678
- ...(stored.flow !== undefined ? { flow: stored.flow } : {}),
679
- body: grantBody(body),
680
- };
844
+ openGrant(raw, signal = this.signal) {
845
+ return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "openGrant", [raw, signal]));
681
846
  }
682
847
  /**
683
848
  * The grant as stored now, migrating layout 2 on first sight. A plaintext
684
849
  * body, written before the vault could seal, is sealed where it lies by
685
850
  * compare-and-set; a failure leaves it readable for the next write to seal.
686
851
  */
687
- async readGrant() {
688
- let raw = await this.storage.get(GRANT);
689
- if (raw === null)
690
- raw = await this.migrate();
691
- if (raw !== null && storedGrant(raw)?.cleanupPending)
692
- raw = await this.cleanupV2(raw);
693
- const grant = await this.openGrant(raw);
694
- if (raw !== null && this.sealer && storedGrant(raw)?.body !== undefined && !this.signal?.aborted) {
695
- const sealed = await this.encodeGrant(grant);
696
- if (await this.storage.compareAndSet(GRANT, raw, sealed).catch(() => false))
697
- raw = sealed;
698
- }
699
- return { raw, grant };
852
+ readGrant(signal = this.signal) {
853
+ return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "readGrant", [signal]));
700
854
  }
701
- /**
702
- * Layout 2's grant, written once as this owner's record. Whichever request
703
- * writes first wins; the layout 2 keys are deleted once a record exists.
704
- */
705
- async migrate() {
706
- const found = await readV2Grant(this.storage, this.sealer);
707
- // A request that has already left writes nothing, this included.
708
- if (!found || this.signal?.aborted)
709
- return null;
710
- const prefix = found.disconnected ? DISCONNECTED_EPOCH_PREFIX : ACTIVE_EPOCH_PREFIX;
711
- const encoded = await this.encodeGrant({
712
- epoch: `${prefix}${crypto.randomUUID()}`,
713
- body: found.body,
714
- cleanupPending: true,
715
- });
716
- let written;
717
- try {
718
- written = await this.storage.compareAndSet(GRANT, null, encoded);
719
- }
720
- catch {
721
- // A store's own error can quote the value it refused.
722
- throw this.credentialWriteError();
723
- }
724
- const raw = written ? encoded : await this.storage.get(GRANT);
725
- return raw;
855
+ /** Layout 2 migration and its cleanup retain the reading operation's owner. */
856
+ migrate(signal) {
857
+ return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "migrate", [signal]));
726
858
  }
727
859
  /** Deletion is retried on every read, including reads after a reset. */
728
- async cleanupV2(raw) {
729
- try {
730
- if (!(await deleteV2Keys(this.storage)))
731
- return raw;
732
- const grant = await this.openGrant(raw);
733
- delete grant.cleanupPending;
734
- const cleared = await this.encodeGrant(grant);
735
- if (await this.storage.compareAndSet(GRANT, raw, cleared))
736
- return cleared;
737
- return this.storage.get(GRANT);
738
- }
739
- catch {
740
- // The durable marker survives both deletion and marker-clear failures.
741
- return raw;
742
- }
860
+ cleanupV2(raw, signal = this.signal) {
861
+ return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "cleanupV2", [raw, signal]));
743
862
  }
744
863
  /** The grant a bound flow reads: a replaced epoch fails it. */
745
- async boundGrant() {
746
- const { grant } = await this.readGrant();
747
- if (this.bound && grant.epoch !== this.epoch)
748
- throw this.flowSuperseded();
749
- return grant;
864
+ boundGrant(signal = this.signal) {
865
+ return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "boundGrant", [signal]));
866
+ }
867
+ /** SDK preparation is call/request-owned; coordinator reads explicitly opt out. */
868
+ activeGrant() {
869
+ return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "activeGrant", []));
750
870
  }
751
871
  /**
752
872
  * The one way a grant changes: read, change, compare-and-set, again while
@@ -756,58 +876,12 @@ export class KvOAuthProvider {
756
876
  * writes after the request's abort too. `change` returning undefined
757
877
  * leaves the grant as it is.
758
878
  */
759
- async updateGrant(change, options = {}) {
760
- for (let attempt = 0; attempt < MAX_GRANT_WRITES; attempt++) {
761
- if (!options.commit && this.signal?.aborted)
762
- return false;
763
- const { raw, grant } = await this.readGrant();
764
- const epoch = options.epoch ?? (this.epoch ??= grant.epoch);
765
- if (grant.epoch !== epoch) {
766
- if (options.quiet || !this.bound)
767
- return false;
768
- throw this.flowSuperseded();
769
- }
770
- // A disconnect's epoch holds nothing until an explicit start replaces it.
771
- if (isDisconnectedEpoch(epoch))
772
- return false;
773
- const next = change(grant);
774
- if (next === undefined)
775
- return true;
776
- const encoded = await this.encodeGrant(next);
777
- if (!options.commit && this.signal?.aborted)
778
- return false;
779
- let written;
780
- try {
781
- written = await this.storage.compareAndSet(GRANT, raw, encoded);
782
- }
783
- catch {
784
- // A store's own error can quote the value it refused.
785
- throw this.credentialWriteError();
786
- }
787
- if (written) {
788
- if (next.epoch !== grant.epoch)
789
- this.epoch = next.epoch;
790
- return true;
791
- }
792
- }
793
- throw this.credentialWriteError();
879
+ updateGrant(change, options = {}) {
880
+ return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "updateGrant", [change, options]));
794
881
  }
795
882
  /** Store a client or tokens for `issuer`'s grant, replacing another server's. */
796
- async writeCredential(field, value, issuer, refreshConsent) {
797
- await this.updateGrant((grant) => {
798
- const body = issuer !== undefined && grant.body.issuer !== issuer ? { issuer } : { ...grant.body };
799
- if (field === "client")
800
- body.client = value;
801
- else {
802
- body.tokens = value;
803
- delete body.refreshConsent;
804
- if (refreshConsent)
805
- body.refreshConsent = refreshConsent;
806
- }
807
- return this.withIssuer(grant, body);
808
- });
809
- // A flow may invalidate what it wrote itself.
810
- this.seen[field] = fingerprint(field === "client" ? value.value : value);
883
+ writeCredential(field, value, issuer, refreshConsent) {
884
+ return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "writeCredential", [field, value, issuer, refreshConsent]));
811
885
  }
812
886
  withIssuer(grant, body) {
813
887
  if (grant.body.issuer !== undefined && grant.body.issuer !== body.issuer) {
@@ -825,21 +899,11 @@ export class KvOAuthProvider {
825
899
  configuredAuthorizationBinding() {
826
900
  return undefined;
827
901
  }
828
- async bindingForGrant(grant) {
829
- const configured = this.configuredAuthorizationBinding();
830
- return configured
831
- ? { ...configured, issuer: grant.body.issuer }
832
- : {
833
- ...(await discoveryBinding(grant.body.discovery)),
834
- issuer: grant.body.issuer,
835
- clientId: grant.body.client?.value.client_id,
836
- };
902
+ bindingForGrant(grant) {
903
+ return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "bindingForGrant", [grant]));
837
904
  }
838
- async checkFlowBinding(grant) {
839
- const binding = this.callback?.flow.binding;
840
- if (!binding || fingerprint(binding) !== fingerprint(await this.bindingForGrant(grant))) {
841
- throw this.flowSuperseded();
842
- }
905
+ checkFlowBinding(grant) {
906
+ return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "checkFlowBinding", [grant]));
843
907
  }
844
908
  /** Validate RFC 9207 against the server this consent selected. */
845
909
  validateCallbackIssuer(issuer) {
@@ -854,73 +918,25 @@ export class KvOAuthProvider {
854
918
  flowLabel(key, epoch) {
855
919
  return `${key}:${epoch}`;
856
920
  }
857
- async encodeFlow(key, flow) {
858
- const { verifier, ...rest } = flow;
859
- const stored = { connectaOAuthFlow: FLOW_VERSION, ...rest };
860
- if (verifier !== undefined) {
861
- if (this.sealer) {
862
- stored.verifier = await this.sealer.seal(this.flowLabel(key, flow.epoch), verifier);
863
- stored.sealed = true;
864
- }
865
- else {
866
- stored.verifier = verifier;
867
- }
868
- }
869
- return JSON.stringify(stored);
921
+ encodeFlow(key, flow) {
922
+ return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "encodeFlow", [key, flow]));
870
923
  }
871
924
  /** A flow record, its verifier opened only when `withVerifier`. */
872
- async openFlow(key, raw, withVerifier) {
873
- const value = parsed(raw);
874
- if (!plainObject(value) ||
875
- value.connectaOAuthFlow !== FLOW_VERSION ||
876
- typeof value.epoch !== "string" ||
877
- typeof value.at !== "number" ||
878
- (value.url !== undefined && typeof value.url !== "string") ||
879
- (value.verifier !== undefined && typeof value.verifier !== "string")) {
880
- return undefined;
881
- }
882
- const flow = {
883
- epoch: value.epoch,
884
- at: value.at,
885
- ...(plainObject(value.binding) ? { binding: value.binding } : {}),
886
- ...(typeof value.url === "string" ? { url: value.url } : {}),
887
- ...(value.consumed === true ? { consumed: true } : {}),
888
- };
889
- if (!withVerifier || typeof value.verifier !== "string")
890
- return flow;
891
- if (value.sealed !== true)
892
- return { ...flow, verifier: value.verifier };
893
- try {
894
- if (!this.sealer)
895
- return undefined;
896
- return { ...flow, verifier: await this.sealer.open(this.flowLabel(key, flow.epoch), value.verifier) };
897
- }
898
- catch {
899
- this.sealer?.warn(`[connecta] connector "${this.connectorId}" has a sealed OAuth consent ` +
900
- "the configured vault cannot open; its callback is refused.");
901
- return undefined;
902
- }
925
+ openFlow(key, raw, withVerifier) {
926
+ return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "openFlow", [key, raw, withVerifier]));
903
927
  }
904
928
  /** The live epoch's latest consent, until a callback claims it. */
905
- async latestConsent() {
906
- const grant = await this.boundGrant();
907
- if (grant.flow === undefined)
908
- return undefined;
909
- const key = oauthFlowKeys.flow(grant.flow);
910
- const raw = await this.storage.get(key);
911
- const flow = raw === null ? undefined : await this.openFlow(key, raw, false);
912
- return flow && !flow.consumed && flow.epoch === grant.epoch && flow.url !== undefined
913
- ? { ...flow, url: flow.url }
914
- : undefined;
929
+ latestConsent() {
930
+ return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "latestConsent", []));
915
931
  }
916
932
  // --- flows and epochs -----------------------------------------------------
917
933
  /** The live epoch; an owner with no grant yet has the initial one. */
918
- async liveEpoch() {
919
- return (await this.readGrant()).grant.epoch;
934
+ liveEpoch() {
935
+ return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "liveEpoch", []));
920
936
  }
921
937
  /** True only after an operator disconnect, until an explicit start replaces it. */
922
- async operatorDisconnected() {
923
- return isDisconnectedEpoch(await this.liveEpoch());
938
+ operatorDisconnected() {
939
+ return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "operatorDisconnected", []));
924
940
  }
925
941
  isOperatorDisconnectedEpoch(epoch) {
926
942
  return isDisconnectedEpoch(epoch);
@@ -930,15 +946,12 @@ export class KvOAuthProvider {
930
946
  * `api()` call or start — in the live epoch, and return it. Nothing about
931
947
  * the grant is decided here: a grant belongs to one server by construction.
932
948
  */
933
- async beginFlow() {
934
- const epoch = await this.liveEpoch();
935
- this.epoch = epoch;
936
- this.bound = true;
937
- return epoch;
949
+ beginFlow() {
950
+ return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "beginFlow", []));
938
951
  }
939
952
  /** The epoch this flow is bound to, or the live one. */
940
- async flowEpoch() {
941
- return this.epoch ?? (await this.liveEpoch());
953
+ flowEpoch() {
954
+ return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "flowEpoch", []));
942
955
  }
943
956
  /**
944
957
  * Bind a code exchange: to the epoch its verified consent was written in, or
@@ -946,25 +959,12 @@ export class KvOAuthProvider {
946
959
  * replaced fails here, before the SDK can send anything. What the grant
947
960
  * holds now is all a refused code may invalidate.
948
961
  */
949
- async bindFlow() {
950
- const { grant } = await this.readGrant();
951
- const epoch = this.callback?.flow.epoch ?? this.epoch ?? grant.epoch;
952
- if (grant.epoch !== epoch)
953
- throw this.flowSuperseded();
954
- this.epoch = epoch;
955
- this.bound = true;
956
- this.codeExchange = true;
957
- await this.checkFlowBinding(grant);
958
- this.seen = {
959
- client: fingerprint(grant.body.client?.value),
960
- tokens: fingerprint(grant.body.tokens),
961
- };
962
+ bindFlow() {
963
+ return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "bindFlow", []));
962
964
  }
963
965
  /** Retain an already verified snapshot only for the exact same state. */
964
- async verifyCallbackState(state) {
965
- if (this.callback?.key === oauthFlowKeys.flow(await oauthStateDigest(state)))
966
- return true;
967
- return this.verifyState(state);
966
+ verifyCallbackState(state) {
967
+ return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "verifyCallbackState", [state]));
968
968
  }
969
969
  /** Whether `verifyState` matched a consent on this provider. */
970
970
  verified() {
@@ -972,73 +972,45 @@ export class KvOAuthProvider {
972
972
  }
973
973
  /** The fixed, retryable failure of a flow a later reset superseded. */
974
974
  flowSuperseded() {
975
- return new ConnectorCallError("unavailable", `Connector "${this.connectorId}" authorization changed while this request was in flight; try again.`);
975
+ return resendAdvice(new ConnectorCallError("unavailable", `Connector "${this.connectorId}" authorization changed while this request was in flight.`), "; try again");
976
976
  }
977
977
  /**
978
978
  * A credential write that storage refused, in fixed text and with no cause
979
979
  * attached. Retryable: the grant it held is untouched.
980
980
  */
981
981
  credentialWriteError() {
982
- return new ConnectorCallError("unavailable", `Connector "${this.connectorId}" could not store its OAuth credentials; try again shortly.`);
982
+ return resendAdvice(new ConnectorCallError("unavailable", `Connector "${this.connectorId}" could not store its OAuth credentials.`), "; try again shortly");
983
983
  }
984
984
  /** @internal Other requests kept refreshing faster than this one could join. */
985
985
  refreshContended() {
986
986
  this.recordRefreshFailure({ kind: "contended" });
987
- return new ConnectorCallError("unavailable", `Connector "${this.connectorId}" could not refresh its OAuth grant while other requests were; retry.`);
987
+ return resendAdvice(new ConnectorCallError("unavailable", `Connector "${this.connectorId}" could not refresh its OAuth grant while other requests were.`), "; retry");
988
988
  }
989
989
  /**
990
990
  * A passive call cannot start consent. A waiter deadline remains retryable;
991
991
  * any dispatched failure requires consent because its token is spent.
992
992
  */
993
993
  authorizationRefused() {
994
+ // The SDK catches a refresh cancellation and falls back to consent. Keep
995
+ // the ended caller's own reason instead of turning it into an auth verdict.
996
+ if (this.signal?.aborted)
997
+ throw aborted(this.signal);
994
998
  const failure = this.refreshFailure;
995
999
  if (failure?.kind === "contended")
996
1000
  return this.refreshContended();
997
1001
  return new UnauthorizedError("Authorization required. Use authorize_connector or Connect to start consent.");
998
1002
  }
999
1003
  // --- the SDK's hooks --------------------------------------------------------
1000
- async clientInformation(ctx) {
1001
- const { body } = await this.boundGrant();
1002
- const client = this.clientOptions?.client;
1003
- if (!body.client && !this.allowAuthorization)
1004
- throw this.authorizationRefused();
1005
- if (client) {
1006
- if ((ctx && ctx.issuer !== client.issuer) || body.issuer !== client.issuer)
1007
- throw this.flowSuperseded();
1008
- if (body.client &&
1009
- (body.client.value.client_id !== client.clientId || body.client.binding !== this.clientBinding))
1010
- throw this.flowSuperseded();
1011
- return {
1012
- client_id: client.clientId,
1013
- issuer: client.issuer,
1014
- token_endpoint_auth_method: this.clientMetadata.token_endpoint_auth_method,
1015
- ...(client.clientSecret !== undefined ? { client_secret: client.clientSecret } : {}),
1016
- };
1017
- }
1018
- if (!body.client || (ctx && body.issuer !== ctx.issuer))
1019
- return undefined;
1020
- this.seen.client = fingerprint(body.client.value);
1021
- return ctx ? { ...body.client.value, issuer: ctx.issuer } : body.client.value;
1022
- }
1023
- async saveClientInformation(info, ctx) {
1024
- const { body } = await this.boundGrant();
1025
- const registrationPath = this.clientOptions?.client
1026
- ? "static"
1027
- : body.client?.value.client_id === info.client_id && body.client.registrationPath
1028
- ? body.client.registrationPath
1029
- : this.clientMetadataUrl &&
1030
- body.discovery?.authorizationServerMetadata?.client_id_metadata_document_supported === true
1031
- ? "cimd"
1032
- : "dcr";
1033
- await this.writeCredential("client", {
1034
- value: this.clientOptions?.client ? { client_id: this.clientOptions.client.clientId } : info,
1035
- registrationPath,
1036
- ...(this.clientBinding !== undefined ? { binding: this.clientBinding } : {}),
1037
- }, ctx?.issuer);
1038
- }
1039
- async registrationPath() {
1040
- const { body } = await this.boundGrant();
1041
- return body.client?.registrationPath;
1004
+ clientInformation(ctx) {
1005
+ return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "clientInformation", [
1006
+ ctx,
1007
+ ]));
1008
+ }
1009
+ saveClientInformation(info, ctx) {
1010
+ return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "saveClientInformation", [info, ctx]));
1011
+ }
1012
+ registrationPath() {
1013
+ return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "registrationPath", []));
1042
1014
  }
1043
1015
  /** Intake and saved URLs share a guard. Include the current grant's secrets
1044
1016
  * so a fresh request can reject an older cache before sending credentials. */
@@ -1068,9 +1040,9 @@ export class KvOAuthProvider {
1068
1040
  secrets.secret(value);
1069
1041
  }
1070
1042
  if (body.client?.value.client_secret)
1071
- secrets.add(body.client.value.client_id);
1043
+ secrets.identifier(body.client.value.client_id);
1072
1044
  if (this.clientOptions?.client?.clientSecret)
1073
- secrets.add(this.clientOptions.client.clientId);
1045
+ secrets.identifier(this.clientOptions.client.clientId);
1074
1046
  const urls = [];
1075
1047
  const collect = (item) => {
1076
1048
  if (typeof item === "string") {
@@ -1086,27 +1058,16 @@ export class KvOAuthProvider {
1086
1058
  collect(field);
1087
1059
  };
1088
1060
  collect(value);
1089
- if ([secrets, this.clientOptions?.secrets].some((source) => source && (source.redact(value) !== value || urls.some((url) => source.containsUrl(url))))) {
1061
+ if ([secrets, this.clientOptions?.secrets].some((source) => source && (source.changes(value) || urls.some((url) => source.containsUrl(url))))) {
1090
1062
  throw new ConnectorCallError("connector_call_failed", "OAuth metadata or consent URL contains a credential; refusing authorization.", { retryable: false });
1091
1063
  }
1092
1064
  }
1093
1065
  /** Only metadata bound to the discovered issuer can supply operator origins. */
1094
- async validatedIssuerOrigin() {
1095
- const { body } = await this.boundGrant();
1096
- const state = body.discovery;
1097
- if (!state ||
1098
- state.authorizationServerMetadata?.issuer !== state.authorizationServerUrl ||
1099
- body.issuer !== state.authorizationServerUrl)
1100
- return undefined;
1101
- this.assertCredentialFree(state, body);
1102
- return new URL(state.authorizationServerUrl).origin;
1066
+ validatedIssuerOrigin() {
1067
+ return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "validatedIssuerOrigin", []));
1103
1068
  }
1104
- async discoveryState() {
1105
- const { body } = await this.boundGrant();
1106
- const state = body.discovery;
1107
- this.assertCredentialFree(state, body);
1108
- this.consentDiscovery = state;
1109
- return state;
1069
+ discoveryState() {
1070
+ return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "discoveryState", []));
1110
1071
  }
1111
1072
  /**
1112
1073
  * Discovery names the grant's server: the same one keeps the client and
@@ -1114,74 +1075,33 @@ export class KvOAuthProvider {
1114
1075
  * without a cached record — a restart's carried client, say — and what a
1115
1076
  * different server leaves behind would never be sent to it anyway.
1116
1077
  */
1117
- async saveDiscoveryState(state) {
1118
- this.assertCredentialFree(state, (await this.boundGrant()).body);
1119
- const issuer = discoveryIssuer(state);
1120
- if (this.clientOptions?.client && issuer !== this.clientOptions.client.issuer)
1121
- throw this.flowSuperseded();
1122
- if (!this.allowAuthorization && !(await this.boundGrant()).body.client)
1123
- throw this.authorizationRefused();
1124
- this.consentDiscovery = state;
1125
- await this.updateGrant((grant) => {
1126
- const body = grant.body.issuer !== issuer ? (issuer !== undefined ? { issuer } : {}) : { ...grant.body };
1127
- this.assertCredentialFree(state, grant.body);
1128
- body.discovery = state;
1129
- return this.withIssuer(grant, body);
1130
- });
1078
+ saveDiscoveryState(state) {
1079
+ return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "saveDiscoveryState", [state]));
1131
1080
  }
1132
1081
  /**
1133
1082
  * The grant's tokens, for the server that issued them only. An
1134
1083
  * issuer-aware read records what it handed the SDK, so the coordinator can
1135
1084
  * tell a refresh another request completed since.
1136
1085
  */
1137
- async tokens(ctx) {
1138
- const grant = await this.boundGrant();
1139
- const { tokens } = grant.body;
1140
- if (!tokens || (ctx && grant.body.issuer !== ctx.issuer))
1141
- return undefined;
1142
- this.seen.tokens = fingerprint(tokens);
1143
- if (!ctx)
1144
- return tokens;
1145
- this.refreshBasis = { accessToken: tokens.access_token, epoch: grant.epoch, issuer: ctx.issuer };
1146
- return { ...tokens, issuer: ctx.issuer };
1147
- }
1148
- async saveTokens(tokens, ctx) {
1149
- if (this.refreshFailure?.kind === "dead")
1150
- throw this.authorizationRefused();
1151
- // The coordinator stored this refresh already, or handed on another's.
1152
- if (this.storedRefresh !== undefined && tokens.access_token === this.storedRefresh)
1153
- return;
1154
- const refreshConsent = this.exchangeConsent &&
1155
- tokens.refresh_token !== undefined &&
1156
- this.exchangeConsent.digest === (await oauthStateDigest(tokens.refresh_token))
1157
- ? this.exchangeConsent
1158
- : undefined;
1159
- await this.writeCredential("tokens", tokens, ctx?.issuer, refreshConsent);
1160
- this.refreshFailure = undefined;
1086
+ tokens(ctx) {
1087
+ return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "tokens", [ctx]));
1088
+ }
1089
+ saveTokens(tokens, ctx) {
1090
+ return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "saveTokens", [tokens, ctx]));
1161
1091
  }
1162
1092
  /**
1163
1093
  * OAuth `state`: 256 random bits the consent URL carries and the callback
1164
1094
  * must present, so nobody holding a consent URL can complete it with their
1165
1095
  * own account (login CSRF). Stored, by digest, with the consent.
1166
1096
  */
1167
- async state() {
1168
- if (!this.allowAuthorization)
1169
- throw this.authorizationRefused();
1170
- const state = randomState();
1171
- this.consent = { state };
1172
- return state;
1173
- }
1174
- async saveCodeVerifier(verifier) {
1175
- if (!this.allowAuthorization)
1176
- throw this.authorizationRefused();
1177
- this.consent = { ...this.consent, verifier };
1178
- }
1179
- async codeVerifier() {
1180
- const verifier = this.callback?.flow.verifier ?? this.consent.verifier;
1181
- if (verifier === undefined) {
1182
- throw new Error(`No PKCE code verifier for "${this.connectorId}"`);
1183
- }
1184
- return verifier;
1097
+ state() {
1098
+ return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "state", []));
1099
+ }
1100
+ saveCodeVerifier(verifier) {
1101
+ return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "saveCodeVerifier", [verifier]));
1102
+ }
1103
+ codeVerifier() {
1104
+ return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "codeVerifier", []));
1185
1105
  }
1186
1106
  /**
1187
1107
  * Store the consent: its flow record first, by state digest with the
@@ -1189,76 +1109,16 @@ export class KvOAuthProvider {
1189
1109
  * reset that replaced the epoch fails the start, and the record is removed,
1190
1110
  * so no start hands out a consent its callback could not complete.
1191
1111
  */
1192
- async redirectToAuthorization(authorizationUrl) {
1193
- if (!this.allowAuthorization)
1194
- throw this.authorizationRefused();
1195
- this.assertCredentialFree(authorizationUrl.toString(), (await this.boundGrant()).body, true);
1196
- const state = this.consent.state ?? authorizationUrl.searchParams.get("state");
1197
- if (!state)
1198
- throw new Error(`OAuth consent for "${this.connectorId}" carries no state`);
1199
- const client = this.clientOptions?.client;
1200
- if (client && !(await this.boundGrant()).body.client) {
1201
- await this.saveClientInformation({ client_id: client.clientId }, { issuer: client.issuer });
1202
- }
1203
- const configured = this.configuredAuthorizationBinding();
1204
- if (configured?.issuer !== undefined) {
1205
- const issuer = configured.issuer;
1206
- // Static adapters do not save discovery or client configuration. Publish
1207
- // their server identity before consent so a changed endpoint fences the
1208
- // old grant before any code exchange can leave.
1209
- await this.updateGrant((grant) => grant.body.issuer === issuer ? undefined : this.withIssuer(grant, { issuer }));
1210
- }
1211
- const epoch = await this.flowEpoch();
1212
- this.epoch = epoch;
1213
- const digest = await oauthStateDigest(state);
1214
- const key = oauthFlowKeys.flow(digest);
1215
- const url = authorizationUrl.toString();
1216
- const { verifier } = this.consent;
1217
- const grant = await this.boundGrant();
1218
- this.assertCredentialFree(url, grant.body, true);
1219
- const binding = this.configuredAuthorizationBinding() ?? {
1220
- ...(await discoveryBinding(this.consentDiscovery ?? grant.body.discovery)),
1221
- issuer: discoveryIssuer(this.consentDiscovery) ?? grant.body.issuer,
1222
- clientId: authorizationUrl.searchParams.get("client_id") ?? grant.body.client?.value.client_id,
1223
- };
1224
- const record = await this.encodeFlow(key, {
1225
- epoch,
1226
- at: Date.now(),
1227
- binding,
1228
- url,
1229
- ...(verifier !== undefined ? { verifier } : {}),
1230
- });
1231
- if (this.signal?.aborted)
1232
- return;
1233
- if (!(await this.storage.compareAndSet(key, null, record, { ttlSeconds: OAUTH_FLOW_TTL_SECONDS }))) {
1234
- throw new Error(`OAuth consent for "${this.connectorId}" reused a state`);
1235
- }
1236
- let published = false;
1237
- try {
1238
- published = await this.updateGrant((grant) => ({ ...grant, flow: digest }), { epoch });
1239
- }
1240
- finally {
1241
- if (!published)
1242
- await this.storage.compareAndSet(key, record, null).catch(() => { });
1243
- }
1244
- if (published)
1245
- this.published = { epoch, url };
1112
+ redirectToAuthorization(authorizationUrl) {
1113
+ return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "redirectToAuthorization", [authorizationUrl]));
1246
1114
  }
1247
1115
  /** The consent this provider stored, while its epoch is still live. */
1248
- async consentUrl() {
1249
- const published = this.published;
1250
- if (!published)
1251
- return undefined;
1252
- if (epochOf(await this.storage.get(GRANT)) !== published.epoch)
1253
- throw this.flowSuperseded();
1254
- this.assertCredentialFree(published.url, (await this.boundGrant()).body, true);
1255
- return published.url;
1116
+ consentUrl(signal = this.signal) {
1117
+ return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "consentUrl", [signal]));
1256
1118
  }
1257
1119
  /** The live epoch's pending consent URL, if one is unclaimed. */
1258
- async pendingAuthorizationUrl() {
1259
- const url = (await this.latestConsent())?.url;
1260
- this.assertCredentialFree(url, (await this.boundGrant()).body, true);
1261
- return url;
1120
+ pendingAuthorizationUrl() {
1121
+ return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "pendingAuthorizationUrl", []));
1262
1122
  }
1263
1123
  /**
1264
1124
  * The pending consent URL, only while it is still worth handing out again:
@@ -1268,39 +1128,16 @@ export class KvOAuthProvider {
1268
1128
  * `invalid_client` drops the client, and its URL would send the operator to
1269
1129
  * a consent the server has already refused.
1270
1130
  */
1271
- async reusablePendingAuthorizationUrl() {
1272
- const consent = await this.latestConsent();
1273
- if (!consent || Math.abs(Date.now() - consent.at) >= PENDING_AUTHORIZATION_MAX_AGE_MS) {
1274
- return undefined;
1275
- }
1276
- this.assertCredentialFree(consent.url, (await this.boundGrant()).body, true);
1277
- let clientId;
1278
- try {
1279
- clientId = new URL(consent.url).searchParams.get("client_id");
1280
- }
1281
- catch {
1282
- return undefined;
1283
- }
1284
- const client = await this.clientInformation();
1285
- return clientId !== null && client?.client_id === clientId ? consent.url : undefined;
1131
+ reusablePendingAuthorizationUrl() {
1132
+ return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "reusablePendingAuthorizationUrl", []));
1286
1133
  }
1287
1134
  /**
1288
1135
  * Find the consent a callback's `state` names. One read whatever the
1289
1136
  * answer; the callback route's refusals pay the same. A claimed consent
1290
1137
  * no longer matches.
1291
1138
  */
1292
- async verifyState(candidate) {
1293
- this.callback = undefined;
1294
- if (candidate === null)
1295
- return false;
1296
- const key = oauthFlowKeys.flow(await oauthStateDigest(candidate));
1297
- const raw = await this.storage.get(key);
1298
- const flow = raw === null ? undefined : await this.openFlow(key, raw, true);
1299
- if (raw === null || !flow || flow.consumed)
1300
- return false;
1301
- this.callback = { key, raw, flow, claimed: false };
1302
- this.epoch = flow.epoch;
1303
- return true;
1139
+ verifyState(candidate) {
1140
+ return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "verifyState", [candidate]));
1304
1141
  }
1305
1142
  /**
1306
1143
  * @internal The first half of the fence a code exchange crosses after every
@@ -1312,43 +1149,15 @@ export class KvOAuthProvider {
1312
1149
  * exchange succeeds; the SDK's one retry after a refused code is answered
1313
1150
  * with that refusal, never sent again.
1314
1151
  */
1315
- async claimCodeExchange() {
1316
- if (this.exchangeRefusal)
1317
- return this.exchangeRefusal.clone();
1318
- await this.claimConsent();
1319
- return undefined;
1152
+ claimCodeExchange() {
1153
+ return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "claimCodeExchange", []));
1320
1154
  }
1321
1155
  /** Terminate the verified consent on an error, sharing the code exchange CAS. */
1322
- async consumeAuthError() {
1323
- const { grant } = await this.readGrant();
1324
- if (!this.callback || grant.epoch !== this.callback.flow.epoch)
1325
- throw this.flowSuperseded();
1326
- await this.checkFlowBinding(grant);
1327
- await this.claimConsent();
1328
- this.callback = undefined;
1329
- }
1330
- async claimConsent() {
1331
- const callback = this.callback;
1332
- if (!callback)
1333
- throw this.flowSuperseded();
1334
- if (callback.claimed)
1335
- throw new OAuthCallbackClaimedError(this.connectorId);
1336
- callback.claimed = true;
1337
- const { epoch, at } = callback.flow;
1338
- const claimed = JSON.stringify({
1339
- connectaOAuthFlow: FLOW_VERSION,
1340
- epoch,
1341
- at,
1342
- consumed: true,
1343
- });
1344
- const remaining = Math.ceil((at + OAUTH_FLOW_TTL_SECONDS * 1000 - Date.now()) / 1000);
1345
- const ttlSeconds = Math.min(OAUTH_FLOW_TTL_SECONDS, Math.max(1, remaining));
1346
- if (await this.storage.compareAndSet(callback.key, callback.raw, claimed, { ttlSeconds })) {
1347
- return;
1348
- }
1349
- if (epochOf(await this.storage.get(GRANT)) !== epoch)
1350
- throw this.flowSuperseded();
1351
- throw new OAuthCallbackClaimedError(this.connectorId);
1156
+ consumeAuthError() {
1157
+ return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "consumeAuthError", []));
1158
+ }
1159
+ claimConsent() {
1160
+ return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "claimConsent", []));
1352
1161
  }
1353
1162
  /**
1354
1163
  * @internal The second half: read the live epoch and, in the reaction to
@@ -1358,86 +1167,24 @@ export class KvOAuthProvider {
1358
1167
  * ordered before the send without a lock across requests, and its tokens
1359
1168
  * are refused by the grant's compare-and-set: one more consent.
1360
1169
  */
1361
- async dispatchCodeExchange(send, endpoint) {
1362
- const epoch = this.epoch;
1363
- if (!this.codeExchange || epoch === undefined)
1364
- throw this.flowSuperseded();
1365
- const raw = await this.storage.get(GRANT);
1366
- const grant = await this.openGrant(raw);
1367
- if (grant.epoch !== epoch)
1368
- throw this.flowSuperseded();
1369
- await this.checkFlowBinding(grant);
1370
- if (endpoint !== undefined && this.callback?.flow.binding?.tokenEndpoint !== String(endpoint)) {
1371
- throw this.flowSuperseded();
1372
- }
1373
- // Opening ciphertext may yield. Re-read the exact record after opening,
1374
- // then check and send in the same reaction, without another await.
1375
- return this.storage.get(GRANT).then((current) => {
1376
- if (current !== raw)
1377
- throw this.flowSuperseded();
1378
- const sent = send();
1379
- sent.catch(() => { });
1380
- return sent;
1381
- });
1170
+ dispatchCodeExchange(send, endpoint) {
1171
+ return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "dispatchCodeExchange", [send, endpoint]));
1382
1172
  }
1383
1173
  /** @internal Keep a refused code's answer for the SDK's retry to read. */
1384
1174
  recordCodeExchangeRefusal(forSdk) {
1385
1175
  this.exchangeRefusal = forSdk.clone();
1386
1176
  }
1387
1177
  /** @internal Observe resolution while accepting the code response, never on a later SDK save. */
1388
- async recordCodeExchangeTokens(tokens) {
1389
- this.exchangeConsent = undefined;
1390
- if (tokens.refresh_token === undefined)
1391
- return;
1392
- try {
1393
- const digest = await oauthStateDigest(tokens.refresh_token);
1394
- const resolution = await this.storage.get(oauthRefreshSpentKeys.spent(digest));
1395
- const spent = storedRefreshSpent(resolution);
1396
- // Observing the exact resolved storage write before consent completion
1397
- // proves their order without comparing isolate clocks. A consent that
1398
- // saw outstanding/ambiguous cannot become eligible on a delayed save.
1399
- if (spent?.state === "resolved")
1400
- this.exchangeConsent = { digest, resolution: resolution };
1401
- }
1402
- catch {
1403
- throw this.credentialWriteError();
1404
- }
1178
+ recordCodeExchangeTokens(tokens) {
1179
+ return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "recordCodeExchangeTokens", [tokens]));
1405
1180
  }
1406
1181
  /**
1407
1182
  * Remove what the SDK asks, but only as this flow saw it: a refused code or
1408
1183
  * refresh says nothing about a client or tokens another flow wrote since.
1409
1184
  * The verifier lives in its consent, which the claim already spent.
1410
1185
  */
1411
- async invalidateCredentials(scope) {
1412
- const fields = {
1413
- all: ["client", "tokens", "discovery"],
1414
- client: ["client"],
1415
- tokens: ["tokens"],
1416
- discovery: ["discovery"],
1417
- verifier: [],
1418
- }[scope];
1419
- if (fields.length > 0) {
1420
- await this.updateGrant((grant) => {
1421
- const body = { ...grant.body };
1422
- let changed = false;
1423
- for (const field of fields) {
1424
- const current = field === "client" ? body.client?.value : body[field];
1425
- if (current === undefined)
1426
- continue;
1427
- if (field !== "discovery" && fingerprint(current) !== this.seen[field])
1428
- continue;
1429
- delete body[field];
1430
- changed = true;
1431
- }
1432
- return changed ? { ...grant, body } : undefined;
1433
- }, { quiet: true });
1434
- }
1435
- // The SDK invalidates after a refused refresh and then starts over, which
1436
- // on `invalid_client` means registering a new client. A passive request
1437
- // must not begin authorization, so it ends here, as auth_required.
1438
- if (!this.allowAuthorization && this.refreshFailure?.kind === "dead") {
1439
- throw this.authorizationRefused();
1440
- }
1186
+ invalidateCredentials(scope) {
1187
+ return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "invalidateCredentials", [scope]));
1441
1188
  }
1442
1189
  // --- refresh, for the coordinator ---------------------------------------------
1443
1190
  /**
@@ -1449,229 +1196,32 @@ export class KvOAuthProvider {
1449
1196
  this.refreshFailure = failure;
1450
1197
  }
1451
1198
  /** @internal A shared-storage claim precedes every refresh dispatch. */
1452
- async claimRefresh(epoch, requested, signal, sharedWait) {
1453
- const waiting = sharedWait ?? refreshWait(signal);
1454
- const wait = waiting.run;
1455
- let delay = 10;
1456
- try {
1457
- const digest = await wait(() => oauthStateDigest(requested ?? ""));
1458
- const key = oauthRefreshKeys.lease(epoch, digest);
1459
- const spentKey = oauthRefreshSpentKeys.spent(digest);
1460
- while (!waiting.signal.aborted) {
1461
- if (signal?.aborted)
1462
- throw aborted(signal);
1463
- const current = await wait(() => this.storedTokens());
1464
- if (current?.refresh_token !== requested || (current && this.refreshedSinceRead(current, epoch))) {
1465
- if (!current)
1466
- this.recordRefreshFailure({ kind: "dead" });
1467
- return current ? this.adoptRefresh(current) : Response.json({ error: "invalid_grant" }, { status: 400 });
1468
- }
1469
- const raw = await wait(() => this.storage.get(key));
1470
- const stored = raw === null ? undefined : parsed(raw);
1471
- if (plainObject(stored) &&
1472
- stored.completed === true &&
1473
- current &&
1474
- stored.tokenDigest === (await wait(() => refreshTokensDigest(current)))) {
1475
- return this.adoptRefresh(current);
1476
- }
1477
- if (plainObject(stored) && (plainObject(stored.verdict) || stored.completed === true)) {
1478
- this.recordRefreshFailure({ kind: "dead" });
1479
- await wait(() => this.discardRefusedGrant(requested, epoch));
1480
- throw new UnauthorizedError("OAuth refresh grant is no longer active.");
1481
- }
1482
- if (plainObject(stored) && stored.state !== "claimed") {
1483
- // Never compare another isolate's clock for a sent request. The
1484
- // durable record survives expiry of this storage-owned liveness key.
1485
- const active = typeof stored.activeKey === "string"
1486
- ? await wait(() => this.storage.get(stored.activeKey))
1487
- : null;
1488
- if (active === null) {
1489
- const refused = JSON.stringify({ ...stored, verdict: { kind: "dead" } });
1490
- if (await wait(() => this.storage.compareAndSet(key, raw, refused))) {
1491
- await wait(() => this.resolveRefreshSpent({ key, spentKey, spentExpected: null, raw: raw }, "ambiguous"));
1492
- this.recordRefreshFailure({ kind: "dead" });
1493
- await wait(() => this.discardRefusedGrant(requested, epoch));
1494
- throw new UnauthorizedError("OAuth refresh outcome is unknown; authorization required.");
1495
- }
1496
- continue;
1497
- }
1498
- }
1499
- else {
1500
- const spentExpected = await wait(() => this.storage.get(spentKey));
1501
- if (spentExpected !== null) {
1502
- const spent = storedRefreshSpent(spentExpected);
1503
- const consent = (await wait(() => this.boundGrant())).body.refreshConsent;
1504
- if (spent?.state !== "resolved" ||
1505
- spent.epoch === epoch ||
1506
- consent?.digest !== digest ||
1507
- consent.resolution !== spentExpected) {
1508
- // Reset can sweep the old lease while its request remains live.
1509
- // Only storage-owned expiry can make an outstanding send ambiguous.
1510
- if (spent?.state === "outstanding" && (await wait(() => this.storage.get(spent.activeKey))) === null) {
1511
- await wait(() => this.storage.compareAndSet(spentKey, spentExpected, JSON.stringify({ ...spent, state: "ambiguous" })));
1512
- }
1513
- this.recordRefreshFailure({ kind: "dead" });
1514
- await wait(() => this.discardRefusedGrant(requested, epoch));
1515
- throw new UnauthorizedError("OAuth refresh token was already spent; authorization required.");
1516
- }
1517
- }
1518
- if (raw === null ||
1519
- (plainObject(stored) && typeof stored.expiresAt === "number" && stored.expiresAt <= this.refreshNow())) {
1520
- // Clock skew can replace an unsent claim; its dispatch CAS then loses.
1521
- const claimed = JSON.stringify({
1522
- connectaOAuthRefresh: 1,
1523
- holder: crypto.randomUUID(),
1524
- state: "claimed",
1525
- expiresAt: this.refreshNow() + OAUTH_REFRESH_LEASE_SECONDS * 1000,
1526
- });
1527
- let won;
1528
- try {
1529
- won = await wait(() => this.storage.compareAndSet(key, raw, claimed));
1530
- }
1531
- catch {
1532
- throw this.credentialWriteError();
1533
- }
1534
- if (won) {
1535
- try {
1536
- const latest = await wait(() => this.storedTokens());
1537
- if (latest?.refresh_token !== requested || (latest && this.refreshedSinceRead(latest, epoch))) {
1538
- await wait(() => this.releaseRefresh({ key, spentKey, spentExpected, raw: claimed }));
1539
- return latest
1540
- ? this.adoptRefresh(latest)
1541
- : Response.json({ error: "invalid_grant" }, { status: 400 });
1542
- }
1543
- if (signal?.aborted)
1544
- throw aborted(signal);
1545
- return { key, spentKey, spentExpected, raw: claimed };
1546
- }
1547
- catch (error) {
1548
- // An unsent claim can expire safely. A caller that has left must
1549
- // settle its local flight without awaiting cleanup storage.
1550
- if (!waiting.signal.aborted && !signal?.aborted) {
1551
- await wait(() => this.releaseRefresh({ key, spentKey, spentExpected, raw: claimed }));
1552
- }
1553
- throw error;
1554
- }
1555
- }
1556
- }
1557
- }
1558
- await runEdge(Effect.raceAllFirst([
1559
- Effect.sleep(delay),
1560
- fromSignal(waiting.signal),
1561
- ...(signal ? [fromSignal(signal)] : []),
1562
- ]));
1563
- delay = Math.min(250, delay * 2);
1564
- }
1565
- }
1566
- catch (error) {
1567
- if (signal?.aborted)
1568
- throw aborted(signal);
1569
- if (!waiting.signal.aborted)
1570
- throw error;
1571
- }
1572
- finally {
1573
- if (!sharedWait)
1574
- waiting.close();
1575
- }
1576
- throw this.refreshContended();
1199
+ claimRefresh(epoch, requested, signal, sharedWait) {
1200
+ return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "claimRefresh", [
1201
+ epoch,
1202
+ requested,
1203
+ signal,
1204
+ sharedWait,
1205
+ ]));
1577
1206
  }
1578
1207
  /** @internal Only the successful claimed-to-dispatched CAS may send. */
1579
- async dispatchRefresh(lease, send, waiting) {
1580
- const stored = parsed(lease.raw);
1581
- const epoch = await waiting.run(() => this.flowEpoch());
1582
- const activeKey = oauthRefreshActiveKeys.holder(epoch, stored.holder);
1583
- await waiting.run(() => this.storage.set(activeKey, JSON.stringify({ connectaOAuthRefresh: 1 }), {
1584
- ttlSeconds: OAUTH_REFRESH_LEASE_SECONDS,
1585
- }));
1586
- const dispatched = JSON.stringify({ ...stored, state: "dispatched", activeKey });
1587
- const transition = this.storage.compareAndSet(lease.key, lease.raw, dispatched).then((won) => {
1588
- if (!won)
1589
- throw this.refreshContended();
1590
- lease.raw = dispatched;
1591
- if (waiting.signal.aborted)
1592
- throw aborted(waiting.signal);
1593
- });
1594
- void transition.catch(() => { });
1595
- await waiting.run(() => transition);
1596
- // One CAS owns the fingerprint across every epoch. Only a later consent
1597
- // that observed a resolved write may replace it. No TTL or deletion path.
1598
- lease.spentRaw = JSON.stringify({
1599
- connectaOAuthRefreshSpent: 1,
1600
- epoch,
1601
- holder: stored.holder,
1602
- activeKey,
1603
- state: "outstanding",
1604
- });
1605
- const preparation = this.storage
1606
- .compareAndSet(lease.spentKey, lease.spentExpected, lease.spentRaw)
1607
- .catch(() => {
1608
- this.recordRefreshFailure({ kind: "dead" });
1609
- throw new UnauthorizedError("OAuth refresh dispatch could not be recorded; authorization required.");
1610
- })
1611
- .then((unspent) => {
1612
- if (!unspent) {
1613
- this.recordRefreshFailure({ kind: "dead" });
1614
- throw new UnauthorizedError("OAuth refresh token was already spent; authorization required.");
1615
- }
1616
- // A storage operation can finish after its waiter left. Never send from
1617
- // abandoned preparation, even if both CAS operations succeeded.
1618
- if (waiting.signal.aborted)
1619
- throw aborted(waiting.signal);
1620
- const sent = send();
1621
- sent.catch(() => { });
1622
- return { sent };
1623
- });
1624
- void preparation.catch(() => { });
1625
- const { sent } = await waiting.run(() => preparation);
1626
- return sent;
1208
+ dispatchRefresh(lease, send, waiting) {
1209
+ return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "dispatchRefresh", [lease, send, waiting]));
1627
1210
  }
1628
1211
  /** @internal Release by CAS after a commit or a classified failure. */
1629
- async releaseRefresh(lease, verdict, tokens, resolution = "ambiguous") {
1630
- const stored = parsed(lease.raw);
1631
- // Only an unsent claim may be removed. Dispatched outcomes never reopen it.
1632
- const next = stored.state === "claimed"
1633
- ? null
1634
- : JSON.stringify({
1635
- ...stored,
1636
- ...(tokens
1637
- ? { completed: true, tokenDigest: await refreshTokensDigest(tokens) }
1638
- : { verdict: { kind: "dead" } }),
1639
- });
1640
- // Verdicts contain fixed typed facts; no downstream causes or text.
1641
- if (verdict)
1642
- this.recordRefreshFailure(verdict);
1643
- await this.resolveRefreshSpent(lease, resolution);
1644
- const released = await this.storage.compareAndSet(lease.key, lease.raw, next).catch(() => false);
1645
- if (released && stored.activeKey)
1646
- await this.storage.delete(stored.activeKey).catch(() => { });
1647
- }
1648
- async resolveRefreshSpent(lease, state) {
1649
- try {
1650
- const raw = lease.spentRaw ?? (await this.storage.get(lease.spentKey));
1651
- const spent = storedRefreshSpent(raw);
1652
- const holder = parsed(lease.raw).holder;
1653
- if (spent?.state !== "outstanding" || spent.holder !== holder)
1654
- return;
1655
- // The timestamp belongs to this resolution write, never to dispatch.
1656
- // Consent copies the exact stored receipt, so wall clocks grant no sends.
1657
- await this.storage.compareAndSet(lease.spentKey, raw, JSON.stringify({
1658
- ...spent,
1659
- state,
1660
- ...(state === "resolved" ? { resolvedAt: Date.now() } : {}),
1661
- }));
1662
- }
1663
- catch {
1664
- // A lost resolution answer leaves outstanding or the durable outcome;
1665
- // either blocks replay until a later consent observes a resolved write.
1666
- }
1212
+ releaseRefresh(lease, verdict, tokens, resolution = "ambiguous") {
1213
+ return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "releaseRefresh", [lease, verdict, tokens, resolution]));
1214
+ }
1215
+ resolveRefreshSpent(lease, state) {
1216
+ return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "resolveRefreshSpent", [lease, state]));
1667
1217
  }
1668
1218
  /** @internal The same typed verdict for local and cross-isolate joiners. */
1669
1219
  refreshVerdict() {
1670
1220
  return this.refreshFailure;
1671
1221
  }
1672
1222
  /** @internal The grant's tokens as stored, in the flow's epoch. */
1673
- async storedTokens() {
1674
- return (await this.boundGrant()).body.tokens;
1223
+ storedTokens(signal = null) {
1224
+ return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "storedTokens", [signal]));
1675
1225
  }
1676
1226
  /** @internal True when a refresh landed after this flow's issuer-aware read. */
1677
1227
  refreshedSinceRead(current, epoch) {
@@ -1694,25 +1244,8 @@ export class KvOAuthProvider {
1694
1244
  * grant, or another epoch is there now. A token response names no issuer:
1695
1245
  * the one the refreshed grant was bound to is stamped here.
1696
1246
  */
1697
- async storeRefresh(epoch, requested, tokens, issuer) {
1698
- for (let attempt = 0; attempt < MAX_GRANT_WRITES; attempt++) {
1699
- try {
1700
- const { raw, grant } = await this.readGrant();
1701
- if (grant.epoch !== epoch || (issuer !== undefined && grant.body.issuer !== issuer))
1702
- return undefined;
1703
- if (grant.body.tokens?.refresh_token !== requested)
1704
- return grant.body.tokens;
1705
- const committed = issuer !== undefined ? { ...tokens, issuer } : tokens;
1706
- const encoded = await this.encodeGrant({ ...grant, body: { ...grant.body, tokens: committed } });
1707
- if (await this.storage.compareAndSet(GRANT, raw, encoded))
1708
- return committed;
1709
- }
1710
- catch {
1711
- // A failed or lost CAS answer may still have committed. Re-read the
1712
- // epoch and retry these in-memory tokens without another HTTP request.
1713
- }
1714
- }
1715
- throw this.credentialWriteError();
1247
+ storeRefresh(epoch, requested, tokens, issuer) {
1248
+ return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "storeRefresh", [epoch, requested, tokens, issuer]));
1716
1249
  }
1717
1250
  /**
1718
1251
  * @internal Drop the tokens the authorization server just refused, only
@@ -1720,18 +1253,13 @@ export class KvOAuthProvider {
1720
1253
  * that completed meanwhile survives. Best effort: if storage fails, the
1721
1254
  * next refresh is refused again and ends the same way.
1722
1255
  */
1723
- async discardRefusedGrant(refreshToken, epoch) {
1724
- try {
1725
- await this.updateGrant((grant) => {
1726
- if (grant.body.tokens?.refresh_token !== refreshToken)
1727
- return undefined;
1728
- const { tokens: _refused, ...body } = grant.body;
1729
- return { ...grant, body };
1730
- }, { epoch, quiet: true, commit: true });
1731
- }
1732
- catch {
1733
- // See above: a refusal that could not be recorded recurs, it does not hide.
1734
- }
1256
+ discardRefusedGrant(refreshToken, epoch, beforeWrite, signal) {
1257
+ return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "discardRefusedGrant", [
1258
+ refreshToken,
1259
+ epoch,
1260
+ beforeWrite,
1261
+ signal,
1262
+ ]));
1735
1263
  }
1736
1264
  // --- restart and disconnect ---------------------------------------------------
1737
1265
  /**
@@ -1743,44 +1271,21 @@ export class KvOAuthProvider {
1743
1271
  * callbacks fail the epoch check anyway and the records expire.
1744
1272
  */
1745
1273
  resetAuthorization(operatorDisconnected = false, preserveClient = false) {
1746
- const reset = this.performReset(operatorDisconnected, preserveClient);
1747
- this.onReset?.(reset);
1748
- return reset;
1274
+ return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "resetAuthorization", [
1275
+ operatorDisconnected,
1276
+ preserveClient,
1277
+ ]));
1749
1278
  }
1750
1279
  /** Remove the local grant before a bounded, best-effort RFC 7009 request. */
1751
1280
  disconnectAuthorization(send) {
1752
- const reset = this.performReset(true, false, send);
1753
- this.onReset?.(reset);
1754
- return reset;
1755
- }
1756
- async performReset(operatorDisconnected, preserveClient, revoke) {
1757
- const epoch = `${operatorDisconnected ? DISCONNECTED_EPOCH_PREFIX : ACTIVE_EPOCH_PREFIX}${crypto.randomUUID()}`;
1758
- for (let attempt = 0; attempt < MAX_GRANT_WRITES; attempt++) {
1759
- const { raw, grant } = await this.readGrant();
1760
- const carried = preserveClient && !operatorDisconnected ? this.carriableClient(grant) : undefined;
1761
- const encoded = await this.encodeGrant({ epoch, body: carried ?? {}, cleanupPending: true });
1762
- let written;
1763
- try {
1764
- written = await this.storage.compareAndSet(GRANT, raw, encoded);
1765
- }
1766
- catch {
1767
- throw this.credentialWriteError();
1768
- }
1769
- if (written) {
1770
- this.refreshCoordinator?.retire(grant.epoch);
1771
- try {
1772
- await this.cleanupV2(encoded);
1773
- await this.sweepConsents();
1774
- await this.sweepRefreshLeases();
1775
- }
1776
- finally {
1777
- if (revoke)
1778
- await revokeDownstreamGrant(grant.body, revoke, this.clientOptions?.client);
1779
- }
1780
- return;
1781
- }
1782
- }
1783
- throw this.credentialWriteError();
1281
+ return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "disconnectAuthorization", [send]));
1282
+ }
1283
+ performReset(operatorDisconnected, preserveClient, revoke) {
1284
+ return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "performReset", [
1285
+ operatorDisconnected,
1286
+ preserveClient,
1287
+ revoke,
1288
+ ]));
1784
1289
  }
1785
1290
  /**
1786
1291
  * Remove every consent of an epoch no longer live. The records are read
@@ -1788,47 +1293,11 @@ export class KvOAuthProvider {
1788
1293
  * the live one then can never complete; each goes by compare-and-set
1789
1294
  * against what was read. Best effort.
1790
1295
  */
1791
- async sweepConsents() {
1792
- try {
1793
- const consents = [];
1794
- for (const key of await this.storage.list(oauthFlowKeys.prefix)) {
1795
- const raw = await this.storage.get(key);
1796
- if (raw !== null)
1797
- consents.push([key, raw]);
1798
- }
1799
- const live = epochOf(await this.storage.get(GRANT));
1800
- for (const [key, raw] of consents) {
1801
- const flow = parsed(raw);
1802
- if (plainObject(flow) && flow.epoch === live)
1803
- continue;
1804
- await this.storage.compareAndSet(key, raw, null);
1805
- }
1806
- }
1807
- catch {
1808
- // Hygiene only: the epoch, not this, fences a stale consent.
1809
- }
1296
+ sweepConsents() {
1297
+ return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "sweepConsents", []));
1810
1298
  }
1811
- async sweepRefreshLeases() {
1812
- try {
1813
- const records = [];
1814
- // A reset may sweep an old lease while its HTTP request is still live.
1815
- // Keep holder liveness until completion or its storage-owned TTL: spent
1816
- // records in later epochs still use it to distinguish live from crashed.
1817
- for (const key of await this.storage.list(oauthRefreshKeys.prefix)) {
1818
- const raw = await this.storage.get(key);
1819
- if (raw !== null)
1820
- records.push([key, raw]);
1821
- }
1822
- const live = epochOf(await this.storage.get(GRANT));
1823
- for (const [key, raw] of records) {
1824
- if (!key.startsWith(`${oauthRefreshKeys.prefix}${live}:`)) {
1825
- await this.storage.compareAndSet(key, raw, null);
1826
- }
1827
- }
1828
- }
1829
- catch {
1830
- // Stale dispatch records cannot grant credentials: epochs never recur.
1831
- }
1299
+ sweepRefreshLeases() {
1300
+ return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "sweepRefreshLeases", []));
1832
1301
  }
1833
1302
  /**
1834
1303
  * The registration a forced restart may carry into its new epoch, or
@@ -1876,4 +1345,1426 @@ export class KvOAuthProvider {
1876
1345
  },
1877
1346
  };
1878
1347
  }
1348
+ static {
1349
+ registerAuthMethod(KvOAuthProvider.prototype, "wait", function (work, signal) {
1350
+ signal = signal === undefined ? this.signal : signal;
1351
+ return authFlow(function* () {
1352
+ if (signal?.aborted)
1353
+ throw aborted(signal);
1354
+ const value = signal
1355
+ ? (yield authAwait(authCall(work, []), { signal }.signal))
1356
+ : (yield authCall(work, []));
1357
+ if (signal?.aborted)
1358
+ throw aborted(signal);
1359
+ return value;
1360
+ });
1361
+ }, KvOAuthProvider.prototype.wait);
1362
+ registerAuthMethod(KvOAuthProvider.prototype, "encodeGrant", function (grant, signal) {
1363
+ signal = signal === undefined ? this.signal : signal;
1364
+ return authFlow(function* () {
1365
+ this.assertCredentialFree({ issuer: grant.body.issuer, discovery: grant.body.discovery }, grant.body);
1366
+ const stored = {
1367
+ connectaOAuth: GRANT_VERSION,
1368
+ epoch: grant.epoch,
1369
+ ...(grant.cleanupPending ? { cleanupPending: true } : {}),
1370
+ ...(grant.flow !== undefined ? { flow: grant.flow } : {}),
1371
+ };
1372
+ if (Object.keys(grant.body).length > 0) {
1373
+ if (this.sealer) {
1374
+ stored.sealed = (yield authInvoke(this, "wait", [
1375
+ authMethod(() => {
1376
+ return authInvoke(this.sealer, "seal", [
1377
+ this.grantLabel(grant.epoch),
1378
+ JSON.stringify(grant.body),
1379
+ ]);
1380
+ }),
1381
+ signal,
1382
+ ]));
1383
+ }
1384
+ else {
1385
+ stored.body = grant.body;
1386
+ }
1387
+ }
1388
+ return JSON.stringify(stored);
1389
+ }.bind(this));
1390
+ }, KvOAuthProvider.prototype.encodeGrant);
1391
+ registerAuthMethod(KvOAuthProvider.prototype, "openGrant", function (raw, signal) {
1392
+ signal = signal === undefined ? this.signal : signal;
1393
+ return authFlow(function* () {
1394
+ if (raw === null)
1395
+ return { epoch: INITIAL_EPOCH, body: {} };
1396
+ const stored = storedGrant(raw);
1397
+ if (!stored)
1398
+ return { epoch: UNREADABLE_EPOCH, body: {} };
1399
+ let body = stored.body;
1400
+ if (stored.sealed !== undefined) {
1401
+ body = undefined;
1402
+ try {
1403
+ if (this.sealer)
1404
+ body = parsed((yield authInvoke(this, "wait", [
1405
+ authMethod(() => {
1406
+ return authInvoke(this.sealer, "open", [
1407
+ this.grantLabel(stored.epoch),
1408
+ stored.sealed,
1409
+ ]);
1410
+ }),
1411
+ signal,
1412
+ ])));
1413
+ }
1414
+ catch {
1415
+ if (signal?.aborted)
1416
+ throw aborted(signal);
1417
+ this.sealer?.warn(`[connecta] connector "${this.connectorId}" has a sealed OAuth grant ` +
1418
+ "the configured vault cannot open; treating it as absent, so the " +
1419
+ "connector needs authorization again. A changed vault key or " +
1420
+ "tampered storage causes this.");
1421
+ }
1422
+ }
1423
+ return {
1424
+ epoch: stored.epoch,
1425
+ ...(stored.cleanupPending ? { cleanupPending: true } : {}),
1426
+ ...(stored.flow !== undefined ? { flow: stored.flow } : {}),
1427
+ body: grantBody(body),
1428
+ };
1429
+ }.bind(this));
1430
+ }, KvOAuthProvider.prototype.openGrant);
1431
+ registerAuthMethod(KvOAuthProvider.prototype, "readGrant", function (signal) {
1432
+ signal = signal === undefined ? this.signal : signal;
1433
+ return authFlow(function* () {
1434
+ let raw = (yield authInvoke(this, "wait", [
1435
+ authMethod(() => {
1436
+ return authStorage(this.storage, "get", [GRANT]);
1437
+ }),
1438
+ signal,
1439
+ ]));
1440
+ if (raw === null)
1441
+ raw = (yield authInvoke(this, "migrate", [signal]));
1442
+ if (raw !== null && storedGrant(raw)?.cleanupPending)
1443
+ raw = (yield authInvoke(this, "cleanupV2", [raw, signal]));
1444
+ const grant = (yield authInvoke(this, "openGrant", [raw, signal]));
1445
+ if (raw !== null && this.sealer && storedGrant(raw)?.body !== undefined) {
1446
+ const sealed = (yield authInvoke(this, "encodeGrant", [grant, signal]));
1447
+ if ((yield authInvoke(this, "wait", [
1448
+ authMethod(() => {
1449
+ return authCatch(authStorage(this.storage, "compareAndSet", [GRANT, raw, sealed]), () => false);
1450
+ }),
1451
+ signal,
1452
+ ])))
1453
+ raw = sealed;
1454
+ }
1455
+ return { raw, grant };
1456
+ }.bind(this));
1457
+ }, KvOAuthProvider.prototype.readGrant);
1458
+ registerAuthMethod(KvOAuthProvider.prototype, "migrate", function (signal) {
1459
+ return authFlow(function* () {
1460
+ const sealer = this.sealer;
1461
+ const found = (yield authInvoke(this, "wait", [
1462
+ authMethod(() => {
1463
+ return authCall(readV2Grant, [
1464
+ this.preparationStorage(signal),
1465
+ sealer && {
1466
+ ...sealer,
1467
+ open: authMethod((label, value) => {
1468
+ return authInvoke(this, "wait", [
1469
+ authMethod(() => {
1470
+ return authInvoke(sealer, "open", [label, value]);
1471
+ }),
1472
+ signal,
1473
+ ]);
1474
+ }),
1475
+ },
1476
+ ]);
1477
+ }),
1478
+ signal,
1479
+ ]));
1480
+ if (!found)
1481
+ return null;
1482
+ const prefix = found.disconnected ? DISCONNECTED_EPOCH_PREFIX : ACTIVE_EPOCH_PREFIX;
1483
+ const encoded = (yield authInvoke(this, "encodeGrant", [
1484
+ {
1485
+ epoch: `${prefix}${crypto.randomUUID()}`,
1486
+ body: found.body,
1487
+ cleanupPending: true,
1488
+ },
1489
+ signal,
1490
+ ]));
1491
+ let written;
1492
+ try {
1493
+ written = (yield authInvoke(this, "wait", [
1494
+ authMethod(() => {
1495
+ return authStorage(this.storage, "compareAndSet", [GRANT, null, encoded]);
1496
+ }),
1497
+ signal,
1498
+ ]));
1499
+ }
1500
+ catch {
1501
+ if (signal?.aborted)
1502
+ throw aborted(signal);
1503
+ throw this.credentialWriteError();
1504
+ }
1505
+ return (yield written
1506
+ ? authValue(() => encoded)
1507
+ : authInvoke(this, "wait", [
1508
+ authMethod(() => {
1509
+ return authStorage(this.storage, "get", [GRANT]);
1510
+ }),
1511
+ signal,
1512
+ ]));
1513
+ }.bind(this));
1514
+ }, KvOAuthProvider.prototype.migrate);
1515
+ registerAuthMethod(KvOAuthProvider.prototype, "cleanupV2", function (raw, signal) {
1516
+ signal = signal === undefined ? this.signal : signal;
1517
+ return authFlow(function* () {
1518
+ try {
1519
+ if (!(yield authInvoke(this, "wait", [
1520
+ authMethod(() => {
1521
+ return authCall(deleteV2Keys, [this.preparationStorage(signal)]);
1522
+ }),
1523
+ signal,
1524
+ ])))
1525
+ return raw;
1526
+ const grant = (yield authInvoke(this, "openGrant", [raw, signal]));
1527
+ delete grant.cleanupPending;
1528
+ const cleared = (yield authInvoke(this, "encodeGrant", [grant, signal]));
1529
+ if ((yield authInvoke(this, "wait", [
1530
+ authMethod(() => {
1531
+ return authStorage(this.storage, "compareAndSet", [GRANT, raw, cleared]);
1532
+ }),
1533
+ signal,
1534
+ ])))
1535
+ return cleared;
1536
+ return (yield authInvoke(this, "wait", [
1537
+ authMethod(() => {
1538
+ return authStorage(this.storage, "get", [GRANT]);
1539
+ }),
1540
+ signal,
1541
+ ]));
1542
+ }
1543
+ catch {
1544
+ if (signal?.aborted)
1545
+ throw aborted(signal);
1546
+ // The durable marker survives both deletion and marker-clear failures.
1547
+ return raw;
1548
+ }
1549
+ }.bind(this));
1550
+ }, KvOAuthProvider.prototype.cleanupV2);
1551
+ registerAuthMethod(KvOAuthProvider.prototype, "boundGrant", function (signal) {
1552
+ signal = signal === undefined ? this.signal : signal;
1553
+ return authFlow(function* () {
1554
+ const { grant } = (yield authInvoke(this, "readGrant", [signal]));
1555
+ if (this.bound && grant.epoch !== this.epoch)
1556
+ throw this.flowSuperseded();
1557
+ return grant;
1558
+ }.bind(this));
1559
+ }, KvOAuthProvider.prototype.boundGrant);
1560
+ registerAuthMethod(KvOAuthProvider.prototype, "activeGrant", function () {
1561
+ return authInvoke(this, "boundGrant", []);
1562
+ }, KvOAuthProvider.prototype.activeGrant);
1563
+ registerAuthMethod(KvOAuthProvider.prototype, "updateGrant", function (change, options = {}) {
1564
+ return authFlow(function* () {
1565
+ const signal = options.signal ?? (options.commit ? null : this.signal);
1566
+ for (let attempt = 0; attempt < MAX_GRANT_WRITES; attempt++) {
1567
+ if (!options.commit && this.signal?.aborted)
1568
+ return false;
1569
+ const { raw, grant } = (yield authInvoke(this, "readGrant", [signal]));
1570
+ const epoch = options.epoch ?? (this.epoch ??= grant.epoch);
1571
+ if (grant.epoch !== epoch) {
1572
+ if (options.quiet || !this.bound)
1573
+ return false;
1574
+ throw this.flowSuperseded();
1575
+ }
1576
+ // A disconnect's epoch holds nothing until an explicit start replaces it.
1577
+ if (isDisconnectedEpoch(epoch))
1578
+ return false;
1579
+ const next = change(grant);
1580
+ if (next === undefined)
1581
+ return true;
1582
+ const encoded = (yield authInvoke(this, "encodeGrant", [next, signal]));
1583
+ if (!options.commit && this.signal?.aborted)
1584
+ return false;
1585
+ let written;
1586
+ try {
1587
+ written = (yield authInvoke(this, "wait", [
1588
+ authMethod(() => {
1589
+ return authStorage(this.storage, "compareAndSet", [GRANT, raw, encoded], options.beforeWrite && (() => options.beforeWrite?.()));
1590
+ }),
1591
+ signal,
1592
+ ]));
1593
+ }
1594
+ catch {
1595
+ if (signal?.aborted)
1596
+ throw aborted(signal);
1597
+ // A store's own error can quote the value it refused.
1598
+ throw this.credentialWriteError();
1599
+ }
1600
+ if (written) {
1601
+ if (next.epoch !== grant.epoch)
1602
+ this.epoch = next.epoch;
1603
+ return true;
1604
+ }
1605
+ }
1606
+ throw this.credentialWriteError();
1607
+ }.bind(this));
1608
+ }, KvOAuthProvider.prototype.updateGrant);
1609
+ registerAuthMethod(KvOAuthProvider.prototype, "writeCredential", function (field, value, issuer, refreshConsent) {
1610
+ return authFlow(function* () {
1611
+ (yield authInvoke(this, "updateGrant", [
1612
+ (grant) => {
1613
+ const body = issuer !== undefined && grant.body.issuer !== issuer ? { issuer } : { ...grant.body };
1614
+ if (field === "client")
1615
+ body.client = value;
1616
+ else {
1617
+ body.tokens = value;
1618
+ delete body.refreshConsent;
1619
+ if (refreshConsent)
1620
+ body.refreshConsent = refreshConsent;
1621
+ }
1622
+ return this.withIssuer(grant, body);
1623
+ },
1624
+ ]));
1625
+ // A flow may invalidate what it wrote itself.
1626
+ this.seen[field] = fingerprint(field === "client" ? value.value : value);
1627
+ }.bind(this));
1628
+ }, KvOAuthProvider.prototype.writeCredential);
1629
+ registerAuthMethod(KvOAuthProvider.prototype, "bindingForGrant", function (grant) {
1630
+ return authFlow(function* () {
1631
+ const configured = this.configuredAuthorizationBinding();
1632
+ return configured
1633
+ ? { ...configured, issuer: grant.body.issuer }
1634
+ : {
1635
+ ...(yield authInvoke(this, "wait", [
1636
+ authMethod(() => {
1637
+ return authCall(discoveryBinding, [grant.body.discovery]);
1638
+ }),
1639
+ ])),
1640
+ issuer: grant.body.issuer,
1641
+ clientId: grant.body.client?.value.client_id,
1642
+ };
1643
+ }.bind(this));
1644
+ }, KvOAuthProvider.prototype.bindingForGrant);
1645
+ registerAuthMethod(KvOAuthProvider.prototype, "checkFlowBinding", function (grant) {
1646
+ return authFlow(function* () {
1647
+ const binding = this.callback?.flow.binding;
1648
+ if (!binding ||
1649
+ fingerprint(binding) !== fingerprint((yield authInvoke(this, "bindingForGrant", [grant])))) {
1650
+ throw this.flowSuperseded();
1651
+ }
1652
+ }.bind(this));
1653
+ }, KvOAuthProvider.prototype.checkFlowBinding);
1654
+ registerAuthMethod(KvOAuthProvider.prototype, "encodeFlow", function (key, flow) {
1655
+ return authFlow(function* () {
1656
+ const { verifier, ...rest } = flow;
1657
+ const stored = { connectaOAuthFlow: FLOW_VERSION, ...rest };
1658
+ if (verifier !== undefined) {
1659
+ if (this.sealer) {
1660
+ stored.verifier = (yield authInvoke(this, "wait", [
1661
+ authMethod(() => {
1662
+ return authInvoke(this.sealer, "seal", [
1663
+ this.flowLabel(key, flow.epoch),
1664
+ verifier,
1665
+ ]);
1666
+ }),
1667
+ ]));
1668
+ stored.sealed = true;
1669
+ }
1670
+ else {
1671
+ stored.verifier = verifier;
1672
+ }
1673
+ }
1674
+ return JSON.stringify(stored);
1675
+ }.bind(this));
1676
+ }, KvOAuthProvider.prototype.encodeFlow);
1677
+ registerAuthMethod(KvOAuthProvider.prototype, "openFlow", function (key, raw, withVerifier) {
1678
+ return authFlow(function* () {
1679
+ const value = parsed(raw);
1680
+ if (!plainObject(value) ||
1681
+ value.connectaOAuthFlow !== FLOW_VERSION ||
1682
+ typeof value.epoch !== "string" ||
1683
+ typeof value.at !== "number" ||
1684
+ (value.url !== undefined && typeof value.url !== "string") ||
1685
+ (value.verifier !== undefined && typeof value.verifier !== "string")) {
1686
+ return undefined;
1687
+ }
1688
+ const flow = {
1689
+ epoch: value.epoch,
1690
+ at: value.at,
1691
+ ...(plainObject(value.binding) ? { binding: value.binding } : {}),
1692
+ ...(typeof value.url === "string" ? { url: value.url } : {}),
1693
+ ...(value.consumed === true ? { consumed: true } : {}),
1694
+ };
1695
+ if (!withVerifier || typeof value.verifier !== "string")
1696
+ return flow;
1697
+ if (value.sealed !== true)
1698
+ return { ...flow, verifier: value.verifier };
1699
+ try {
1700
+ if (!this.sealer)
1701
+ return undefined;
1702
+ return {
1703
+ ...flow,
1704
+ verifier: (yield authInvoke(this, "wait", [
1705
+ authMethod(() => {
1706
+ return authInvoke(this.sealer, "open", [
1707
+ this.flowLabel(key, flow.epoch),
1708
+ value.verifier,
1709
+ ]);
1710
+ }),
1711
+ ])),
1712
+ };
1713
+ }
1714
+ catch {
1715
+ if (this.signal?.aborted)
1716
+ throw aborted(this.signal);
1717
+ this.sealer?.warn(`[connecta] connector "${this.connectorId}" has a sealed OAuth consent ` +
1718
+ "the configured vault cannot open; its callback is refused.");
1719
+ return undefined;
1720
+ }
1721
+ }.bind(this));
1722
+ }, KvOAuthProvider.prototype.openFlow);
1723
+ registerAuthMethod(KvOAuthProvider.prototype, "latestConsent", function () {
1724
+ return authFlow(function* () {
1725
+ const grant = (yield authInvoke(this, "boundGrant", []));
1726
+ if (grant.flow === undefined)
1727
+ return undefined;
1728
+ const key = oauthFlowKeys.flow(grant.flow);
1729
+ const raw = (yield authInvoke(this, "wait", [
1730
+ authMethod(() => {
1731
+ return authStorage(this.storage, "get", [key]);
1732
+ }),
1733
+ ]));
1734
+ const flow = raw === null ? undefined : (yield authInvoke(this, "openFlow", [key, raw, false]));
1735
+ return flow && !flow.consumed && flow.epoch === grant.epoch && flow.url !== undefined
1736
+ ? { ...flow, url: flow.url }
1737
+ : undefined;
1738
+ }.bind(this));
1739
+ }, KvOAuthProvider.prototype.latestConsent);
1740
+ registerAuthMethod(KvOAuthProvider.prototype, "liveEpoch", function () {
1741
+ return authFlow(function* () {
1742
+ return (yield authInvoke(this, "readGrant", [])).grant.epoch;
1743
+ }.bind(this));
1744
+ }, KvOAuthProvider.prototype.liveEpoch);
1745
+ registerAuthMethod(KvOAuthProvider.prototype, "operatorDisconnected", function () {
1746
+ return authFlow(function* () {
1747
+ return isDisconnectedEpoch((yield authInvoke(this, "liveEpoch", [])));
1748
+ }.bind(this));
1749
+ }, KvOAuthProvider.prototype.operatorDisconnected);
1750
+ registerAuthMethod(KvOAuthProvider.prototype, "beginFlow", function () {
1751
+ return authFlow(function* () {
1752
+ const epoch = (yield authInvoke(this, "liveEpoch", []));
1753
+ this.epoch = epoch;
1754
+ this.bound = true;
1755
+ return epoch;
1756
+ }.bind(this));
1757
+ }, KvOAuthProvider.prototype.beginFlow);
1758
+ registerAuthMethod(KvOAuthProvider.prototype, "flowEpoch", function () {
1759
+ return authFlow(function* () {
1760
+ return this.epoch ?? (yield authInvoke(this, "liveEpoch", []));
1761
+ }.bind(this));
1762
+ }, KvOAuthProvider.prototype.flowEpoch);
1763
+ registerAuthMethod(KvOAuthProvider.prototype, "bindFlow", function () {
1764
+ return authFlow(function* () {
1765
+ const { grant } = (yield authInvoke(this, "readGrant", []));
1766
+ const epoch = this.callback?.flow.epoch ?? this.epoch ?? grant.epoch;
1767
+ if (grant.epoch !== epoch)
1768
+ throw this.flowSuperseded();
1769
+ this.epoch = epoch;
1770
+ this.bound = true;
1771
+ this.codeExchange = true;
1772
+ (yield authInvoke(this, "checkFlowBinding", [grant]));
1773
+ this.seen = {
1774
+ client: fingerprint(grant.body.client?.value),
1775
+ tokens: fingerprint(grant.body.tokens),
1776
+ };
1777
+ }.bind(this));
1778
+ }, KvOAuthProvider.prototype.bindFlow);
1779
+ registerAuthMethod(KvOAuthProvider.prototype, "verifyCallbackState", function (state) {
1780
+ return authFlow(function* () {
1781
+ if (this.callback?.key ===
1782
+ oauthFlowKeys.flow((yield authInvoke(this, "wait", [
1783
+ authMethod(() => {
1784
+ return authCall(oauthStateDigest, [state]);
1785
+ }),
1786
+ ]))))
1787
+ return true;
1788
+ return (yield authInvoke(this, "verifyState", [state]));
1789
+ }.bind(this));
1790
+ }, KvOAuthProvider.prototype.verifyCallbackState);
1791
+ registerAuthMethod(KvOAuthProvider.prototype, "clientInformation", function (ctx) {
1792
+ return authFlow(function* () {
1793
+ if (this.signal?.aborted)
1794
+ throw aborted(this.signal);
1795
+ const { body } = (yield authInvoke(this, "activeGrant", []));
1796
+ if (this.signal?.aborted)
1797
+ throw aborted(this.signal);
1798
+ const client = this.clientOptions?.client;
1799
+ if (!body.client && !this.allowAuthorization)
1800
+ throw this.authorizationRefused();
1801
+ if (client) {
1802
+ if ((ctx && ctx.issuer !== client.issuer) || body.issuer !== client.issuer)
1803
+ throw this.flowSuperseded();
1804
+ if (body.client &&
1805
+ (body.client.value.client_id !== client.clientId || body.client.binding !== this.clientBinding))
1806
+ throw this.flowSuperseded();
1807
+ return {
1808
+ client_id: client.clientId,
1809
+ issuer: client.issuer,
1810
+ token_endpoint_auth_method: this.clientMetadata.token_endpoint_auth_method,
1811
+ ...(client.clientSecret !== undefined ? { client_secret: client.clientSecret } : {}),
1812
+ };
1813
+ }
1814
+ if (!body.client || (ctx && body.issuer !== ctx.issuer))
1815
+ return undefined;
1816
+ this.seen.client = fingerprint(body.client.value);
1817
+ return ctx ? { ...body.client.value, issuer: ctx.issuer } : body.client.value;
1818
+ }.bind(this));
1819
+ }, KvOAuthProvider.prototype.clientInformation);
1820
+ registerAuthMethod(KvOAuthProvider.prototype, "saveClientInformation", function (info, ctx) {
1821
+ return authFlow(function* () {
1822
+ try {
1823
+ const { body } = (yield authInvoke(this, "boundGrant", []));
1824
+ const registrationPath = this.clientOptions?.client
1825
+ ? "static"
1826
+ : body.client?.value.client_id === info.client_id && body.client.registrationPath
1827
+ ? body.client.registrationPath
1828
+ : this.clientMetadataUrl &&
1829
+ body.discovery?.authorizationServerMetadata?.client_id_metadata_document_supported === true
1830
+ ? "cimd"
1831
+ : "dcr";
1832
+ (yield authInvoke(this, "writeCredential", [
1833
+ "client",
1834
+ {
1835
+ value: this.clientOptions?.client ? { client_id: this.clientOptions.client.clientId } : info,
1836
+ registrationPath,
1837
+ ...(this.clientBinding !== undefined ? { binding: this.clientBinding } : {}),
1838
+ },
1839
+ ctx?.issuer,
1840
+ ]));
1841
+ }
1842
+ catch (error) {
1843
+ if (!this.signal?.aborted)
1844
+ throw error;
1845
+ }
1846
+ }.bind(this));
1847
+ }, KvOAuthProvider.prototype.saveClientInformation);
1848
+ registerAuthMethod(KvOAuthProvider.prototype, "registrationPath", function () {
1849
+ return authFlow(function* () {
1850
+ const { body } = (yield authInvoke(this, "boundGrant", []));
1851
+ return body.client?.registrationPath;
1852
+ }.bind(this));
1853
+ }, KvOAuthProvider.prototype.registrationPath);
1854
+ registerAuthMethod(KvOAuthProvider.prototype, "validatedIssuerOrigin", function () {
1855
+ return authFlow(function* () {
1856
+ const { body } = (yield authInvoke(this, "boundGrant", []));
1857
+ const state = body.discovery;
1858
+ if (!state ||
1859
+ state.authorizationServerMetadata?.issuer !== state.authorizationServerUrl ||
1860
+ body.issuer !== state.authorizationServerUrl)
1861
+ return undefined;
1862
+ this.assertCredentialFree(state, body);
1863
+ return new URL(state.authorizationServerUrl).origin;
1864
+ }.bind(this));
1865
+ }, KvOAuthProvider.prototype.validatedIssuerOrigin);
1866
+ registerAuthMethod(KvOAuthProvider.prototype, "discoveryState", function () {
1867
+ return authFlow(function* () {
1868
+ if (this.signal?.aborted)
1869
+ throw aborted(this.signal);
1870
+ const { body } = (yield authInvoke(this, "activeGrant", []));
1871
+ if (this.signal?.aborted)
1872
+ throw aborted(this.signal);
1873
+ const state = body.discovery;
1874
+ this.assertCredentialFree(state, body);
1875
+ this.consentDiscovery = state;
1876
+ return state;
1877
+ }.bind(this));
1878
+ }, KvOAuthProvider.prototype.discoveryState);
1879
+ registerAuthMethod(KvOAuthProvider.prototype, "saveDiscoveryState", function (state) {
1880
+ return authFlow(function* () {
1881
+ this.assertCredentialFree(state, (yield authInvoke(this, "boundGrant", [])).body);
1882
+ const issuer = discoveryIssuer(state);
1883
+ if (this.clientOptions?.client && issuer !== this.clientOptions.client.issuer)
1884
+ throw this.flowSuperseded();
1885
+ if (!this.allowAuthorization && !(yield authInvoke(this, "boundGrant", [])).body.client)
1886
+ throw this.authorizationRefused();
1887
+ this.consentDiscovery = state;
1888
+ (yield authInvoke(this, "updateGrant", [
1889
+ (grant) => {
1890
+ const body = grant.body.issuer !== issuer ? (issuer !== undefined ? { issuer } : {}) : { ...grant.body };
1891
+ this.assertCredentialFree(state, grant.body);
1892
+ body.discovery = state;
1893
+ return this.withIssuer(grant, body);
1894
+ },
1895
+ ]));
1896
+ }.bind(this));
1897
+ }, KvOAuthProvider.prototype.saveDiscoveryState);
1898
+ registerAuthMethod(KvOAuthProvider.prototype, "tokens", function (ctx) {
1899
+ return authFlow(function* () {
1900
+ if (this.signal?.aborted)
1901
+ throw aborted(this.signal);
1902
+ const grant = (yield authInvoke(this, "activeGrant", []));
1903
+ if (this.signal?.aborted)
1904
+ throw aborted(this.signal);
1905
+ const { tokens } = grant.body;
1906
+ if (!tokens || (ctx && grant.body.issuer !== ctx.issuer))
1907
+ return undefined;
1908
+ this.seen.tokens = fingerprint(tokens);
1909
+ if (!ctx)
1910
+ return tokens;
1911
+ this.refreshBasis = { accessToken: tokens.access_token, epoch: grant.epoch, issuer: ctx.issuer };
1912
+ return { ...tokens, issuer: ctx.issuer };
1913
+ }.bind(this));
1914
+ }, KvOAuthProvider.prototype.tokens);
1915
+ registerAuthMethod(KvOAuthProvider.prototype, "saveTokens", function (tokens, ctx) {
1916
+ return authFlow(function* () {
1917
+ if (this.signal?.aborted)
1918
+ throw aborted(this.signal);
1919
+ if (this.refreshFailure?.kind === "dead")
1920
+ throw this.authorizationRefused();
1921
+ // The coordinator stored this refresh already, or handed on another's.
1922
+ if (this.storedRefresh !== undefined && tokens.access_token === this.storedRefresh)
1923
+ return;
1924
+ const refreshConsent = this.exchangeConsent &&
1925
+ tokens.refresh_token !== undefined &&
1926
+ this.exchangeConsent.digest ===
1927
+ (yield authInvoke(this, "wait", [
1928
+ authMethod(() => {
1929
+ return authCall(oauthStateDigest, [tokens.refresh_token]);
1930
+ }),
1931
+ ]))
1932
+ ? this.exchangeConsent
1933
+ : undefined;
1934
+ (yield authInvoke(this, "writeCredential", ["tokens", tokens, ctx?.issuer, refreshConsent]));
1935
+ this.refreshFailure = undefined;
1936
+ }.bind(this));
1937
+ }, KvOAuthProvider.prototype.saveTokens);
1938
+ registerAuthMethod(KvOAuthProvider.prototype, "state", function () {
1939
+ return authValue(() => {
1940
+ if (!this.allowAuthorization)
1941
+ throw this.authorizationRefused();
1942
+ const state = randomState();
1943
+ this.consent = { state };
1944
+ return state;
1945
+ });
1946
+ }, KvOAuthProvider.prototype.state);
1947
+ registerAuthMethod(KvOAuthProvider.prototype, "saveCodeVerifier", function (verifier) {
1948
+ return authValue(() => {
1949
+ if (this.signal?.aborted)
1950
+ throw aborted(this.signal);
1951
+ if (!this.allowAuthorization)
1952
+ throw this.authorizationRefused();
1953
+ this.consent = { ...this.consent, verifier };
1954
+ });
1955
+ }, KvOAuthProvider.prototype.saveCodeVerifier);
1956
+ registerAuthMethod(KvOAuthProvider.prototype, "codeVerifier", function () {
1957
+ return authValue(() => {
1958
+ const verifier = this.callback?.flow.verifier ?? this.consent.verifier;
1959
+ if (verifier === undefined) {
1960
+ throw new Error(`No PKCE code verifier for "${this.connectorId}"`);
1961
+ }
1962
+ return verifier;
1963
+ });
1964
+ }, KvOAuthProvider.prototype.codeVerifier);
1965
+ registerAuthMethod(KvOAuthProvider.prototype, "redirectToAuthorization", function (authorizationUrl) {
1966
+ return authFlow(function* () {
1967
+ if (this.signal?.aborted)
1968
+ return;
1969
+ if (!this.allowAuthorization)
1970
+ throw this.authorizationRefused();
1971
+ this.assertCredentialFree(authorizationUrl.toString(), (yield authInvoke(this, "boundGrant", [])).body, true);
1972
+ const state = this.consent.state ?? authorizationUrl.searchParams.get("state");
1973
+ if (!state)
1974
+ throw new Error(`OAuth consent for "${this.connectorId}" carries no state`);
1975
+ const client = this.clientOptions?.client;
1976
+ if (client && !(yield authInvoke(this, "boundGrant", [])).body.client) {
1977
+ (yield authInvoke(this, "saveClientInformation", [
1978
+ { client_id: client.clientId },
1979
+ { issuer: client.issuer },
1980
+ ]));
1981
+ }
1982
+ const configured = this.configuredAuthorizationBinding();
1983
+ if (configured?.issuer !== undefined) {
1984
+ const issuer = configured.issuer;
1985
+ // Static adapters do not save discovery or client configuration. Publish
1986
+ // their server identity before consent so a changed endpoint fences the
1987
+ // old grant before any code exchange can leave.
1988
+ (yield authInvoke(this, "updateGrant", [
1989
+ (grant) => (grant.body.issuer === issuer ? undefined : this.withIssuer(grant, { issuer })),
1990
+ ]));
1991
+ }
1992
+ const epoch = (yield authInvoke(this, "flowEpoch", []));
1993
+ this.epoch = epoch;
1994
+ const digest = (yield authInvoke(this, "wait", [
1995
+ authMethod(() => {
1996
+ return authCall(oauthStateDigest, [state]);
1997
+ }),
1998
+ ]));
1999
+ const key = oauthFlowKeys.flow(digest);
2000
+ const url = authorizationUrl.toString();
2001
+ const { verifier } = this.consent;
2002
+ const grant = (yield authInvoke(this, "boundGrant", []));
2003
+ this.assertCredentialFree(url, grant.body, true);
2004
+ const binding = this.configuredAuthorizationBinding() ?? {
2005
+ ...(yield authInvoke(this, "wait", [
2006
+ authMethod(() => {
2007
+ return authCall(discoveryBinding, [this.consentDiscovery ?? grant.body.discovery]);
2008
+ }),
2009
+ ])),
2010
+ issuer: discoveryIssuer(this.consentDiscovery) ?? grant.body.issuer,
2011
+ clientId: authorizationUrl.searchParams.get("client_id") ?? grant.body.client?.value.client_id,
2012
+ };
2013
+ const record = (yield authInvoke(this, "encodeFlow", [
2014
+ key,
2015
+ {
2016
+ epoch,
2017
+ at: Date.now(),
2018
+ binding,
2019
+ url,
2020
+ ...(verifier !== undefined ? { verifier } : {}),
2021
+ },
2022
+ ]));
2023
+ if (this.signal?.aborted)
2024
+ return;
2025
+ if (!(yield authInvoke(this, "wait", [
2026
+ authMethod(() => {
2027
+ return authStorage(this.storage, "compareAndSet", [
2028
+ key,
2029
+ null,
2030
+ record,
2031
+ { ttlSeconds: OAUTH_FLOW_TTL_SECONDS },
2032
+ ]);
2033
+ }),
2034
+ ]))) {
2035
+ throw new Error(`OAuth consent for "${this.connectorId}" reused a state`);
2036
+ }
2037
+ let published = false;
2038
+ try {
2039
+ published = (yield authInvoke(this, "updateGrant", [
2040
+ (grant) => ({ ...grant, flow: digest }),
2041
+ { epoch },
2042
+ ]));
2043
+ }
2044
+ finally {
2045
+ if (!published)
2046
+ (yield authCatch(authInvoke(this, "wait", [
2047
+ authMethod(() => {
2048
+ return authStorage(this.storage, "compareAndSet", [key, record, null]);
2049
+ }),
2050
+ ]), () => { }));
2051
+ }
2052
+ if (published)
2053
+ this.published = { epoch, url };
2054
+ }.bind(this));
2055
+ }, KvOAuthProvider.prototype.redirectToAuthorization);
2056
+ registerAuthMethod(KvOAuthProvider.prototype, "consentUrl", function (signal) {
2057
+ signal = signal === undefined ? this.signal : signal;
2058
+ return authFlow(function* () {
2059
+ const published = this.published;
2060
+ if (!published)
2061
+ return undefined;
2062
+ if (epochOf((yield authInvoke(this, "wait", [
2063
+ authMethod(() => {
2064
+ return authStorage(this.storage, "get", [GRANT]);
2065
+ }),
2066
+ signal,
2067
+ ]))) !== published.epoch)
2068
+ throw this.flowSuperseded();
2069
+ this.assertCredentialFree(published.url, (yield authInvoke(this, "boundGrant", [signal])).body, true);
2070
+ return published.url;
2071
+ }.bind(this));
2072
+ }, KvOAuthProvider.prototype.consentUrl);
2073
+ registerAuthMethod(KvOAuthProvider.prototype, "pendingAuthorizationUrl", function () {
2074
+ return authFlow(function* () {
2075
+ const url = (yield authInvoke(this, "latestConsent", []))?.url;
2076
+ this.assertCredentialFree(url, (yield authInvoke(this, "boundGrant", [])).body, true);
2077
+ return url;
2078
+ }.bind(this));
2079
+ }, KvOAuthProvider.prototype.pendingAuthorizationUrl);
2080
+ registerAuthMethod(KvOAuthProvider.prototype, "reusablePendingAuthorizationUrl", function () {
2081
+ return authFlow(function* () {
2082
+ const consent = (yield authInvoke(this, "latestConsent", []));
2083
+ if (!consent || Math.abs(Date.now() - consent.at) >= PENDING_AUTHORIZATION_MAX_AGE_MS) {
2084
+ return undefined;
2085
+ }
2086
+ this.assertCredentialFree(consent.url, (yield authInvoke(this, "boundGrant", [])).body, true);
2087
+ let clientId;
2088
+ try {
2089
+ clientId = new URL(consent.url).searchParams.get("client_id");
2090
+ }
2091
+ catch {
2092
+ return undefined;
2093
+ }
2094
+ const client = (yield authInvoke(this, "clientInformation", []));
2095
+ return clientId !== null && client?.client_id === clientId ? consent.url : undefined;
2096
+ }.bind(this));
2097
+ }, KvOAuthProvider.prototype.reusablePendingAuthorizationUrl);
2098
+ registerAuthMethod(KvOAuthProvider.prototype, "verifyState", function (candidate) {
2099
+ return authFlow(function* () {
2100
+ this.callback = undefined;
2101
+ if (candidate === null)
2102
+ return false;
2103
+ const key = oauthFlowKeys.flow((yield authInvoke(this, "wait", [
2104
+ authMethod(() => {
2105
+ return authCall(oauthStateDigest, [candidate]);
2106
+ }),
2107
+ ])));
2108
+ const raw = (yield authInvoke(this, "wait", [
2109
+ authMethod(() => {
2110
+ return authStorage(this.storage, "get", [key]);
2111
+ }),
2112
+ ]));
2113
+ const flow = raw === null ? undefined : (yield authInvoke(this, "openFlow", [key, raw, true]));
2114
+ if (raw === null || !flow || flow.consumed)
2115
+ return false;
2116
+ this.callback = { key, raw, flow, claimed: false };
2117
+ this.epoch = flow.epoch;
2118
+ return true;
2119
+ }.bind(this));
2120
+ }, KvOAuthProvider.prototype.verifyState);
2121
+ registerAuthMethod(KvOAuthProvider.prototype, "claimCodeExchange", function () {
2122
+ return authFlow(function* () {
2123
+ if (this.exchangeRefusal)
2124
+ return this.exchangeRefusal.clone();
2125
+ (yield authInvoke(this, "claimConsent", []));
2126
+ return undefined;
2127
+ }.bind(this));
2128
+ }, KvOAuthProvider.prototype.claimCodeExchange);
2129
+ registerAuthMethod(KvOAuthProvider.prototype, "consumeAuthError", function () {
2130
+ return authFlow(function* () {
2131
+ const { grant } = (yield authInvoke(this, "readGrant", []));
2132
+ if (!this.callback || grant.epoch !== this.callback.flow.epoch)
2133
+ throw this.flowSuperseded();
2134
+ (yield authInvoke(this, "checkFlowBinding", [grant]));
2135
+ (yield authInvoke(this, "claimConsent", []));
2136
+ this.callback = undefined;
2137
+ }.bind(this));
2138
+ }, KvOAuthProvider.prototype.consumeAuthError);
2139
+ registerAuthMethod(KvOAuthProvider.prototype, "claimConsent", function () {
2140
+ return authFlow(function* () {
2141
+ const callback = this.callback;
2142
+ if (!callback)
2143
+ throw this.flowSuperseded();
2144
+ if (callback.claimed)
2145
+ throw new OAuthCallbackClaimedError(this.connectorId);
2146
+ callback.claimed = true;
2147
+ const { epoch, at } = callback.flow;
2148
+ const claimed = JSON.stringify({
2149
+ connectaOAuthFlow: FLOW_VERSION,
2150
+ epoch,
2151
+ at,
2152
+ consumed: true,
2153
+ });
2154
+ const remaining = Math.ceil((at + OAUTH_FLOW_TTL_SECONDS * 1000 - Date.now()) / 1000);
2155
+ const ttlSeconds = Math.min(OAUTH_FLOW_TTL_SECONDS, Math.max(1, remaining));
2156
+ if ((yield authInvoke(this, "wait", [
2157
+ authMethod(() => {
2158
+ return authStorage(this.storage, "compareAndSet", [
2159
+ callback.key,
2160
+ callback.raw,
2161
+ claimed,
2162
+ { ttlSeconds },
2163
+ ]);
2164
+ }),
2165
+ ]))) {
2166
+ return;
2167
+ }
2168
+ if (epochOf((yield authInvoke(this, "wait", [
2169
+ authMethod(() => {
2170
+ return authStorage(this.storage, "get", [GRANT]);
2171
+ }),
2172
+ ]))) !== epoch)
2173
+ throw this.flowSuperseded();
2174
+ throw new OAuthCallbackClaimedError(this.connectorId);
2175
+ }.bind(this));
2176
+ }, KvOAuthProvider.prototype.claimConsent);
2177
+ registerAuthMethod(KvOAuthProvider.prototype, "dispatchCodeExchange", function (send, endpoint) {
2178
+ return authFlow(function* () {
2179
+ const epoch = this.epoch;
2180
+ if (!this.codeExchange || epoch === undefined)
2181
+ throw this.flowSuperseded();
2182
+ const raw = (yield authInvoke(this, "wait", [
2183
+ authMethod(() => {
2184
+ return authStorage(this.storage, "get", [GRANT]);
2185
+ }),
2186
+ ]));
2187
+ const grant = (yield authInvoke(this, "openGrant", [raw]));
2188
+ if (grant.epoch !== epoch)
2189
+ throw this.flowSuperseded();
2190
+ (yield authInvoke(this, "checkFlowBinding", [grant]));
2191
+ if (endpoint !== undefined && this.callback?.flow.binding?.tokenEndpoint !== String(endpoint)) {
2192
+ throw this.flowSuperseded();
2193
+ }
2194
+ // Opening ciphertext may yield. Re-read the exact record after opening,
2195
+ // then check and send in the same reaction, without another await.
2196
+ return (yield authInvoke(this, "wait", [
2197
+ authMethod(() => {
2198
+ return authThen(authStorage(this.storage, "get", [GRANT]), authMethod((current) => {
2199
+ return authFlow(function* () {
2200
+ if (this.signal?.aborted)
2201
+ throw aborted(this.signal);
2202
+ if (current !== raw)
2203
+ throw this.flowSuperseded();
2204
+ const sent = send();
2205
+ sent.catch(() => { });
2206
+ return (yield authValue(() => sent));
2207
+ }.bind(this));
2208
+ }));
2209
+ }),
2210
+ ]));
2211
+ }.bind(this));
2212
+ }, KvOAuthProvider.prototype.dispatchCodeExchange);
2213
+ registerAuthMethod(KvOAuthProvider.prototype, "recordCodeExchangeTokens", function (tokens) {
2214
+ return authFlow(function* () {
2215
+ this.exchangeConsent = undefined;
2216
+ if (tokens.refresh_token === undefined)
2217
+ return;
2218
+ try {
2219
+ const digest = (yield authInvoke(this, "wait", [
2220
+ authMethod(() => {
2221
+ return authCall(oauthStateDigest, [tokens.refresh_token]);
2222
+ }),
2223
+ ]));
2224
+ const resolution = (yield authInvoke(this, "wait", [
2225
+ authMethod(() => {
2226
+ return authStorage(this.storage, "get", [oauthRefreshSpentKeys.spent(digest)]);
2227
+ }),
2228
+ ]));
2229
+ const spent = storedRefreshSpent(resolution);
2230
+ // Observing the exact resolved storage write before consent completion
2231
+ // proves their order without comparing isolate clocks. A consent that
2232
+ // saw outstanding/ambiguous cannot become eligible on a delayed save.
2233
+ if (spent?.state === "resolved")
2234
+ this.exchangeConsent = { digest, resolution: resolution };
2235
+ }
2236
+ catch {
2237
+ if (this.signal?.aborted)
2238
+ throw aborted(this.signal);
2239
+ throw this.credentialWriteError();
2240
+ }
2241
+ }.bind(this));
2242
+ }, KvOAuthProvider.prototype.recordCodeExchangeTokens);
2243
+ registerAuthMethod(KvOAuthProvider.prototype, "invalidateCredentials", function (scope) {
2244
+ return authFlow(function* () {
2245
+ if (this.signal?.aborted)
2246
+ throw aborted(this.signal);
2247
+ const fields = {
2248
+ all: ["client", "tokens", "discovery"],
2249
+ client: ["client"],
2250
+ tokens: ["tokens"],
2251
+ discovery: ["discovery"],
2252
+ verifier: [],
2253
+ }[scope];
2254
+ if (fields.length > 0) {
2255
+ (yield authInvoke(this, "updateGrant", [
2256
+ (grant) => {
2257
+ const body = { ...grant.body };
2258
+ let changed = false;
2259
+ for (const field of fields) {
2260
+ const current = field === "client" ? body.client?.value : body[field];
2261
+ if (current === undefined)
2262
+ continue;
2263
+ if (field !== "discovery" && fingerprint(current) !== this.seen[field])
2264
+ continue;
2265
+ delete body[field];
2266
+ changed = true;
2267
+ }
2268
+ return changed ? { ...grant, body } : undefined;
2269
+ },
2270
+ { quiet: true },
2271
+ ]));
2272
+ }
2273
+ // The SDK invalidates after a refused refresh and then starts over, which
2274
+ // on `invalid_client` means registering a new client. A passive request
2275
+ // must not begin authorization, so it ends here, as auth_required.
2276
+ if (!this.allowAuthorization && this.refreshFailure?.kind === "dead") {
2277
+ throw this.authorizationRefused();
2278
+ }
2279
+ }.bind(this));
2280
+ }, KvOAuthProvider.prototype.invalidateCredentials);
2281
+ registerAuthMethod(KvOAuthProvider.prototype, "claimRefresh", function (epoch, requested, signal, sharedWait) {
2282
+ return authFlow(function* () {
2283
+ const waiting = sharedWait ?? refreshWait(signal);
2284
+ const wait = waiting.run;
2285
+ let delay = 10;
2286
+ try {
2287
+ const digest = (yield authCall(wait, [
2288
+ authMethod(() => {
2289
+ return authCall(oauthStateDigest, [requested ?? ""]);
2290
+ }),
2291
+ ]));
2292
+ const key = oauthRefreshKeys.lease(epoch, digest);
2293
+ const spentKey = oauthRefreshSpentKeys.spent(digest);
2294
+ while (!waiting.signal.aborted) {
2295
+ const current = (yield authCall(wait, [
2296
+ authMethod(() => {
2297
+ return authInvoke(this, "storedTokens", [waiting.signal]);
2298
+ }),
2299
+ ]));
2300
+ if (current?.refresh_token !== requested || (current && this.refreshedSinceRead(current, epoch))) {
2301
+ if (!current)
2302
+ this.recordRefreshFailure({ kind: "dead" });
2303
+ return current
2304
+ ? this.adoptRefresh(current)
2305
+ : Response.json({ error: "invalid_grant" }, { status: 400 });
2306
+ }
2307
+ const raw = (yield authCall(wait, [
2308
+ authMethod(() => {
2309
+ return authStorage(this.storage, "get", [key]);
2310
+ }),
2311
+ ]));
2312
+ const stored = raw === null ? undefined : parsed(raw);
2313
+ if (plainObject(stored) &&
2314
+ stored.completed === true &&
2315
+ current &&
2316
+ stored.tokenDigest ===
2317
+ (yield authCall(wait, [
2318
+ authMethod(() => {
2319
+ return authCall(refreshTokensDigest, [current]);
2320
+ }),
2321
+ ]))) {
2322
+ return this.adoptRefresh(current);
2323
+ }
2324
+ if (plainObject(stored) && (plainObject(stored.verdict) || stored.completed === true)) {
2325
+ this.recordRefreshFailure({ kind: "dead" });
2326
+ (yield authCall(wait, [
2327
+ authMethod(() => {
2328
+ return authInvoke(this, "discardRefusedGrant", [
2329
+ requested,
2330
+ epoch,
2331
+ waiting.transfer,
2332
+ waiting.signal,
2333
+ ]);
2334
+ }),
2335
+ ]));
2336
+ throw new UnauthorizedError("OAuth refresh grant is no longer active.");
2337
+ }
2338
+ if (plainObject(stored) && stored.state !== "claimed") {
2339
+ // Never compare another isolate's clock for a sent request. The
2340
+ // durable record survives expiry of this storage-owned liveness key.
2341
+ const active = typeof stored.activeKey === "string"
2342
+ ? (yield authCall(wait, [
2343
+ authMethod(() => {
2344
+ return authStorage(this.storage, "get", [stored.activeKey]);
2345
+ }),
2346
+ ]))
2347
+ : null;
2348
+ if (active === null) {
2349
+ const refused = JSON.stringify({ ...stored, verdict: { kind: "dead" } });
2350
+ if ((yield authCall(wait, [
2351
+ authMethod(() => {
2352
+ return authStorage(this.storage, "compareAndSet", [key, raw, refused], () => waiting.transfer());
2353
+ }),
2354
+ ]))) {
2355
+ (yield authCall(wait, [
2356
+ authMethod(() => {
2357
+ return authInvoke(this, "resolveRefreshSpent", [
2358
+ { key, spentKey, spentExpected: null, raw: raw },
2359
+ "ambiguous",
2360
+ ]);
2361
+ }),
2362
+ ]));
2363
+ this.recordRefreshFailure({ kind: "dead" });
2364
+ (yield authCall(wait, [
2365
+ authMethod(() => {
2366
+ return authInvoke(this, "discardRefusedGrant", [
2367
+ requested,
2368
+ epoch,
2369
+ waiting.transfer,
2370
+ waiting.signal,
2371
+ ]);
2372
+ }),
2373
+ ]));
2374
+ throw new UnauthorizedError("OAuth refresh outcome is unknown; authorization required.");
2375
+ }
2376
+ continue;
2377
+ }
2378
+ }
2379
+ else {
2380
+ const spentExpected = (yield authCall(wait, [
2381
+ authMethod(() => {
2382
+ return authStorage(this.storage, "get", [spentKey]);
2383
+ }),
2384
+ ]));
2385
+ if (spentExpected !== null) {
2386
+ const spent = storedRefreshSpent(spentExpected);
2387
+ const consent = (yield authCall(wait, [
2388
+ authMethod(() => {
2389
+ return authInvoke(this, "boundGrant", [waiting.signal]);
2390
+ }),
2391
+ ])).body.refreshConsent;
2392
+ if (spent?.state !== "resolved" ||
2393
+ spent.epoch === epoch ||
2394
+ consent?.digest !== digest ||
2395
+ consent.resolution !== spentExpected) {
2396
+ // Reset can sweep the old lease while its request remains live.
2397
+ // Only storage-owned expiry can make an outstanding send ambiguous.
2398
+ if (spent?.state === "outstanding" &&
2399
+ (yield authCall(wait, [
2400
+ authMethod(() => {
2401
+ return authStorage(this.storage, "get", [spent.activeKey]);
2402
+ }),
2403
+ ])) === null) {
2404
+ (yield authCall(wait, [
2405
+ authMethod(() => {
2406
+ return authStorage(this.storage, "compareAndSet", [spentKey, spentExpected, JSON.stringify({ ...spent, state: "ambiguous" })], () => waiting.transfer());
2407
+ }),
2408
+ ]));
2409
+ }
2410
+ this.recordRefreshFailure({ kind: "dead" });
2411
+ (yield authCall(wait, [
2412
+ authMethod(() => {
2413
+ return authInvoke(this, "discardRefusedGrant", [
2414
+ requested,
2415
+ epoch,
2416
+ waiting.transfer,
2417
+ waiting.signal,
2418
+ ]);
2419
+ }),
2420
+ ]));
2421
+ throw new UnauthorizedError("OAuth refresh token was already spent; authorization required.");
2422
+ }
2423
+ }
2424
+ if (raw === null ||
2425
+ (plainObject(stored) &&
2426
+ typeof stored.expiresAt === "number" &&
2427
+ stored.expiresAt <= this.refreshNow())) {
2428
+ // Clock skew can replace an unsent claim; its dispatch CAS then loses.
2429
+ const claimed = JSON.stringify({
2430
+ connectaOAuthRefresh: 1,
2431
+ holder: crypto.randomUUID(),
2432
+ state: "claimed",
2433
+ expiresAt: this.refreshNow() + OAUTH_REFRESH_LEASE_SECONDS * 1000,
2434
+ });
2435
+ let won;
2436
+ try {
2437
+ won = (yield authCall(wait, [
2438
+ authMethod(() => {
2439
+ return authStorage(this.storage, "compareAndSet", [key, raw, claimed], () => waiting.transfer());
2440
+ }),
2441
+ ]));
2442
+ }
2443
+ catch {
2444
+ throw this.credentialWriteError();
2445
+ }
2446
+ if (won) {
2447
+ try {
2448
+ const latest = (yield authCall(wait, [
2449
+ authMethod(() => {
2450
+ return authInvoke(this, "storedTokens", [waiting.signal]);
2451
+ }),
2452
+ ]));
2453
+ if (latest?.refresh_token !== requested || (latest && this.refreshedSinceRead(latest, epoch))) {
2454
+ (yield authCall(wait, [
2455
+ authMethod(() => {
2456
+ return authInvoke(this, "releaseRefresh", [
2457
+ { key, spentKey, spentExpected, raw: claimed },
2458
+ ]);
2459
+ }),
2460
+ ]));
2461
+ return latest
2462
+ ? this.adoptRefresh(latest)
2463
+ : Response.json({ error: "invalid_grant" }, { status: 400 });
2464
+ }
2465
+ return { key, spentKey, spentExpected, raw: claimed };
2466
+ }
2467
+ catch (error) {
2468
+ if (!waiting.signal.aborted) {
2469
+ (yield authCall(wait, [
2470
+ authMethod(() => {
2471
+ return authInvoke(this, "releaseRefresh", [
2472
+ { key, spentKey, spentExpected, raw: claimed },
2473
+ ]);
2474
+ }),
2475
+ ]));
2476
+ }
2477
+ throw error;
2478
+ }
2479
+ }
2480
+ }
2481
+ }
2482
+ (yield Effect.raceAllFirst([Effect.sleep(delay), fromSignal(waiting.signal)]));
2483
+ delay = Math.min(250, delay * 2);
2484
+ }
2485
+ }
2486
+ catch (error) {
2487
+ if (!waiting.signal.aborted)
2488
+ throw error;
2489
+ }
2490
+ finally {
2491
+ if (!sharedWait)
2492
+ waiting.close();
2493
+ }
2494
+ throw this.refreshContended();
2495
+ }.bind(this));
2496
+ }, KvOAuthProvider.prototype.claimRefresh);
2497
+ registerAuthMethod(KvOAuthProvider.prototype, "dispatchRefresh", function (lease, send, waiting) {
2498
+ return authFlow(function* () {
2499
+ const stored = parsed(lease.raw);
2500
+ const epoch = (yield authInvoke(waiting, "run", [
2501
+ authMethod(() => {
2502
+ return authInvoke(this, "flowEpoch", []);
2503
+ }),
2504
+ ]));
2505
+ const activeKey = oauthRefreshActiveKeys.holder(epoch, stored.holder);
2506
+ (yield authInvoke(waiting, "run", [
2507
+ authMethod(() => {
2508
+ return authStorage(this.storage, "set", [
2509
+ activeKey,
2510
+ JSON.stringify({ connectaOAuthRefresh: 1 }),
2511
+ {
2512
+ ttlSeconds: OAUTH_REFRESH_LEASE_SECONDS,
2513
+ },
2514
+ ]);
2515
+ }),
2516
+ ]));
2517
+ const dispatched = JSON.stringify({
2518
+ ...stored,
2519
+ state: "dispatched",
2520
+ activeKey,
2521
+ });
2522
+ const won = (yield authInvoke(waiting, "run", [
2523
+ authMethod(() => authStorage(this.storage, "compareAndSet", [lease.key, lease.raw, dispatched])),
2524
+ ]));
2525
+ if (!won)
2526
+ throw this.refreshContended();
2527
+ lease.raw = dispatched;
2528
+ if (waiting.signal.aborted)
2529
+ throw aborted(waiting.signal);
2530
+ // One CAS owns the fingerprint across every epoch. Only a later consent
2531
+ // that observed a resolved write may replace it. No TTL or deletion path.
2532
+ lease.spentRaw = JSON.stringify({
2533
+ connectaOAuthRefreshSpent: 1,
2534
+ epoch,
2535
+ holder: stored.holder,
2536
+ activeKey,
2537
+ state: "outstanding",
2538
+ });
2539
+ let unspent;
2540
+ try {
2541
+ unspent = (yield authInvoke(waiting, "run", [
2542
+ authMethod(() => authStorage(this.storage, "compareAndSet", [
2543
+ lease.spentKey,
2544
+ lease.spentExpected,
2545
+ lease.spentRaw,
2546
+ ])),
2547
+ ]));
2548
+ }
2549
+ catch {
2550
+ this.recordRefreshFailure({ kind: "dead" });
2551
+ throw new UnauthorizedError("OAuth refresh dispatch could not be recorded; authorization required.");
2552
+ }
2553
+ if (!unspent) {
2554
+ this.recordRefreshFailure({ kind: "dead" });
2555
+ throw new UnauthorizedError("OAuth refresh token was already spent; authorization required.");
2556
+ }
2557
+ // Final owner check and physical submission share one reaction.
2558
+ // Submission already transferred ownership to the coordinator. Only
2559
+ // its own deadline can stop this send; the departed caller cannot.
2560
+ if (waiting.signal.aborted)
2561
+ throw aborted(waiting.signal);
2562
+ return (yield authCall(send, []));
2563
+ }.bind(this));
2564
+ }, KvOAuthProvider.prototype.dispatchRefresh);
2565
+ registerAuthMethod(KvOAuthProvider.prototype, "releaseRefresh", function (lease, verdict, tokens, resolution = "ambiguous") {
2566
+ return authFlow(function* () {
2567
+ const stored = parsed(lease.raw);
2568
+ // Only an unsent claim may be removed. Dispatched outcomes never reopen it.
2569
+ const next = stored.state === "claimed"
2570
+ ? null
2571
+ : JSON.stringify({
2572
+ ...stored,
2573
+ ...(tokens
2574
+ ? {
2575
+ completed: true,
2576
+ tokenDigest: (yield authCall(refreshTokensDigest, [tokens])),
2577
+ }
2578
+ : { verdict: { kind: "dead" } }),
2579
+ });
2580
+ // Verdicts contain fixed typed facts; no downstream causes or text.
2581
+ if (verdict)
2582
+ this.recordRefreshFailure(verdict);
2583
+ (yield authInvoke(this, "resolveRefreshSpent", [lease, resolution]));
2584
+ const released = (yield authCatch(authStorage(this.storage, "compareAndSet", [lease.key, lease.raw, next]), () => false));
2585
+ if (released && stored.activeKey)
2586
+ (yield authCatch(authStorage(this.storage, "delete", [stored.activeKey]), () => { }));
2587
+ }.bind(this));
2588
+ }, KvOAuthProvider.prototype.releaseRefresh);
2589
+ registerAuthMethod(KvOAuthProvider.prototype, "resolveRefreshSpent", function (lease, state) {
2590
+ return authFlow(function* () {
2591
+ try {
2592
+ const raw = lease.spentRaw ?? (yield authStorage(this.storage, "get", [lease.spentKey]));
2593
+ const spent = storedRefreshSpent(raw);
2594
+ const holder = parsed(lease.raw).holder;
2595
+ if (spent?.state !== "outstanding" || spent.holder !== holder)
2596
+ return;
2597
+ // The timestamp belongs to this resolution write, never to dispatch.
2598
+ // Consent copies the exact stored receipt, so wall clocks grant no sends.
2599
+ (yield authStorage(this.storage, "compareAndSet", [
2600
+ lease.spentKey,
2601
+ raw,
2602
+ JSON.stringify({
2603
+ ...spent,
2604
+ state,
2605
+ ...(state === "resolved" ? { resolvedAt: Date.now() } : {}),
2606
+ }),
2607
+ ]));
2608
+ }
2609
+ catch {
2610
+ // A lost resolution answer leaves outstanding or the durable outcome;
2611
+ // either blocks replay until a later consent observes a resolved write.
2612
+ }
2613
+ }.bind(this));
2614
+ }, KvOAuthProvider.prototype.resolveRefreshSpent);
2615
+ registerAuthMethod(KvOAuthProvider.prototype, "storedTokens", function (signal = null) {
2616
+ return authFlow(function* () {
2617
+ return (yield authInvoke(this, "boundGrant", [signal])).body.tokens;
2618
+ }.bind(this));
2619
+ }, KvOAuthProvider.prototype.storedTokens);
2620
+ registerAuthMethod(KvOAuthProvider.prototype, "storeRefresh", function (epoch, requested, tokens, issuer) {
2621
+ return authFlow(function* () {
2622
+ for (let attempt = 0; attempt < MAX_GRANT_WRITES; attempt++) {
2623
+ try {
2624
+ const { raw, grant } = (yield authInvoke(this, "readGrant", [null]));
2625
+ if (grant.epoch !== epoch || (issuer !== undefined && grant.body.issuer !== issuer))
2626
+ return undefined;
2627
+ if (grant.body.tokens?.refresh_token !== requested)
2628
+ return grant.body.tokens;
2629
+ const committed = issuer !== undefined ? { ...tokens, issuer } : tokens;
2630
+ const encoded = (yield authInvoke(this, "encodeGrant", [
2631
+ { ...grant, body: { ...grant.body, tokens: committed } },
2632
+ null,
2633
+ ]));
2634
+ if ((yield authStorage(this.storage, "compareAndSet", [GRANT, raw, encoded])))
2635
+ return committed;
2636
+ }
2637
+ catch {
2638
+ // A failed or lost CAS answer may still have committed. Re-read the
2639
+ // epoch and retry these in-memory tokens without another HTTP request.
2640
+ }
2641
+ }
2642
+ throw this.credentialWriteError();
2643
+ }.bind(this));
2644
+ }, KvOAuthProvider.prototype.storeRefresh);
2645
+ registerAuthMethod(KvOAuthProvider.prototype, "discardRefusedGrant", function (refreshToken, epoch, beforeWrite, signal) {
2646
+ return authFlow(function* () {
2647
+ try {
2648
+ (yield authInvoke(this, "updateGrant", [
2649
+ (grant) => {
2650
+ if (grant.body.tokens?.refresh_token !== refreshToken)
2651
+ return undefined;
2652
+ const { tokens: _refused, ...body } = grant.body;
2653
+ return { ...grant, body };
2654
+ },
2655
+ {
2656
+ epoch,
2657
+ quiet: true,
2658
+ commit: true,
2659
+ ...(beforeWrite ? { beforeWrite } : {}),
2660
+ ...(signal ? { signal } : {}),
2661
+ },
2662
+ ]));
2663
+ }
2664
+ catch {
2665
+ // See above: a refusal that could not be recorded recurs, it does not hide.
2666
+ }
2667
+ }.bind(this));
2668
+ }, KvOAuthProvider.prototype.discardRefusedGrant);
2669
+ registerAuthMethod(KvOAuthProvider.prototype, "resetAuthorization", function (operatorDisconnected = false, preserveClient = false) {
2670
+ return authFlow(function* () {
2671
+ const operation = authInvoke(this, "performReset", [operatorDisconnected, preserveClient]);
2672
+ if (!this.onReset)
2673
+ return (yield operation);
2674
+ const reset = runEdge(operation);
2675
+ this.onReset?.(reset);
2676
+ return (yield authValue(() => reset));
2677
+ }.bind(this));
2678
+ }, KvOAuthProvider.prototype.resetAuthorization);
2679
+ registerAuthMethod(KvOAuthProvider.prototype, "disconnectAuthorization", function (send) {
2680
+ return authFlow(function* () {
2681
+ const operation = authInvoke(this, "performReset", [true, false, send]);
2682
+ if (!this.onReset)
2683
+ return (yield operation);
2684
+ const reset = runEdge(operation);
2685
+ this.onReset?.(reset);
2686
+ return (yield authValue(() => reset));
2687
+ }.bind(this));
2688
+ }, KvOAuthProvider.prototype.disconnectAuthorization);
2689
+ registerAuthMethod(KvOAuthProvider.prototype, "performReset", function (operatorDisconnected, preserveClient, revoke) {
2690
+ return authFlow(function* () {
2691
+ const epoch = `${operatorDisconnected ? DISCONNECTED_EPOCH_PREFIX : ACTIVE_EPOCH_PREFIX}${crypto.randomUUID()}`;
2692
+ for (let attempt = 0; attempt < MAX_GRANT_WRITES; attempt++) {
2693
+ const { raw, grant } = (yield authInvoke(this, "readGrant", [null]));
2694
+ const carried = preserveClient && !operatorDisconnected ? this.carriableClient(grant) : undefined;
2695
+ const encoded = (yield authInvoke(this, "encodeGrant", [
2696
+ { epoch, body: carried ?? {}, cleanupPending: true },
2697
+ null,
2698
+ ]));
2699
+ let written;
2700
+ try {
2701
+ written = (yield authStorage(this.storage, "compareAndSet", [GRANT, raw, encoded]));
2702
+ }
2703
+ catch {
2704
+ throw this.credentialWriteError();
2705
+ }
2706
+ if (written) {
2707
+ this.refreshCoordinator?.retire(grant.epoch);
2708
+ try {
2709
+ (yield authInvoke(this, "cleanupV2", [encoded, null]));
2710
+ (yield authInvoke(this, "sweepConsents", []));
2711
+ (yield authInvoke(this, "sweepRefreshLeases", []));
2712
+ }
2713
+ finally {
2714
+ if (revoke)
2715
+ (yield authCall(revokeDownstreamGrant, [grant.body, revoke, this.clientOptions?.client]));
2716
+ }
2717
+ return;
2718
+ }
2719
+ }
2720
+ throw this.credentialWriteError();
2721
+ }.bind(this));
2722
+ }, KvOAuthProvider.prototype.performReset);
2723
+ registerAuthMethod(KvOAuthProvider.prototype, "sweepConsents", function () {
2724
+ return authFlow(function* () {
2725
+ try {
2726
+ const consents = [];
2727
+ for (const key of (yield authStorage(this.storage, "list", [oauthFlowKeys.prefix]))) {
2728
+ const raw = (yield authStorage(this.storage, "get", [key]));
2729
+ if (raw !== null)
2730
+ consents.push([key, raw]);
2731
+ }
2732
+ const live = epochOf((yield authStorage(this.storage, "get", [GRANT])));
2733
+ for (const [key, raw] of consents) {
2734
+ const flow = parsed(raw);
2735
+ if (plainObject(flow) && flow.epoch === live)
2736
+ continue;
2737
+ (yield authStorage(this.storage, "compareAndSet", [key, raw, null]));
2738
+ }
2739
+ }
2740
+ catch {
2741
+ // Hygiene only: the epoch, not this, fences a stale consent.
2742
+ }
2743
+ }.bind(this));
2744
+ }, KvOAuthProvider.prototype.sweepConsents);
2745
+ registerAuthMethod(KvOAuthProvider.prototype, "sweepRefreshLeases", function () {
2746
+ return authFlow(function* () {
2747
+ try {
2748
+ const records = [];
2749
+ // A reset may sweep an old lease while its HTTP request is still live.
2750
+ // Keep holder liveness until completion or its storage-owned TTL: spent
2751
+ // records in later epochs still use it to distinguish live from crashed.
2752
+ for (const key of (yield authStorage(this.storage, "list", [oauthRefreshKeys.prefix]))) {
2753
+ const raw = (yield authStorage(this.storage, "get", [key]));
2754
+ if (raw !== null)
2755
+ records.push([key, raw]);
2756
+ }
2757
+ const live = epochOf((yield authStorage(this.storage, "get", [GRANT])));
2758
+ for (const [key, raw] of records) {
2759
+ if (!key.startsWith(`${oauthRefreshKeys.prefix}${live}:`)) {
2760
+ (yield authStorage(this.storage, "compareAndSet", [key, raw, null]));
2761
+ }
2762
+ }
2763
+ }
2764
+ catch {
2765
+ // Stale dispatch records cannot grant credentials: epochs never recur.
2766
+ }
2767
+ }.bind(this));
2768
+ }, KvOAuthProvider.prototype.sweepRefreshLeases);
2769
+ }
1879
2770
  }