@apifuse/provider-sdk 2.2.0-beta.5 → 2.2.0-beta.50

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 (365) hide show
  1. package/AUTHORING.md +537 -33
  2. package/CHANGELOG.md +201 -1
  3. package/README.md +83 -22
  4. package/SUBMISSION.md +2 -2
  5. package/bin/apifuse-check.ts +165 -11
  6. package/bin/apifuse-dev.ts +57 -13
  7. package/bin/apifuse-migrate-operation-declaration.ts +55 -0
  8. package/bin/apifuse-migrate-shape.ts +202 -0
  9. package/bin/apifuse-pack-check.ts +22 -2
  10. package/bin/apifuse-pack-smoke.ts +57 -2
  11. package/bin/apifuse-pack-types.ts +357 -38
  12. package/bin/apifuse-perf.ts +14 -13
  13. package/bin/apifuse-record.ts +709 -72
  14. package/bin/apifuse-submit-check.ts +2344 -324
  15. package/bin/apifuse-sync-assets.ts +117 -0
  16. package/bin/submit-check-delimited-text.ts +50 -0
  17. package/dist/auth-turn/index.d.ts +3 -3
  18. package/dist/auth-turn/index.js +1 -1
  19. package/dist/auth.d.ts +14 -0
  20. package/dist/auth.js +67 -0
  21. package/dist/ceremonies/index.d.ts +16 -0
  22. package/dist/ceremonies/index.js +141 -36
  23. package/dist/cli/commands.d.ts +1 -1
  24. package/dist/cli/commands.js +27 -0
  25. package/dist/cli/create.d.ts +4 -1
  26. package/dist/cli/create.js +42 -37
  27. package/dist/cli/migrate-operation-declaration.d.ts +59 -0
  28. package/dist/cli/migrate-operation-declaration.js +1178 -0
  29. package/dist/cli/migrate-operation-shape.d.ts +44 -0
  30. package/dist/cli/migrate-operation-shape.js +113 -0
  31. package/dist/cli/migrate-provider-shape.d.ts +52 -0
  32. package/dist/cli/migrate-provider-shape.js +578 -0
  33. package/dist/cli/prompt-assets.d.ts +80 -0
  34. package/dist/cli/prompt-assets.js +743 -0
  35. package/dist/cli/templates/provider/AGENTS.md.tpl +17 -8
  36. package/dist/cli/templates/provider/Dockerfile.tpl +1 -1
  37. package/dist/cli/templates/provider/README.md.tpl +5 -5
  38. package/dist/cli/templates/provider/index.ts.tpl +6 -3
  39. package/dist/cli/templates/provider/operations/ping.ts.tpl +4 -1
  40. package/dist/cli/templates/provider/provider.json.tpl +6 -0
  41. package/dist/config/loader.d.ts +179 -16
  42. package/dist/config/loader.js +435 -127
  43. package/dist/contract-serialization.js +4 -8
  44. package/dist/contract-types.d.ts +12 -5
  45. package/dist/contract.js +24 -10
  46. package/dist/declaration-validation.d.ts +33 -0
  47. package/dist/declaration-validation.js +266 -0
  48. package/dist/define.d.ts +66 -29
  49. package/dist/define.js +778 -69
  50. package/dist/dev.d.ts +3 -0
  51. package/dist/dev.js +1 -1
  52. package/dist/engine.d.ts +78 -0
  53. package/dist/engine.js +133 -0
  54. package/dist/error-observability.d.ts +7 -0
  55. package/dist/error-observability.js +61 -0
  56. package/dist/error-resolution.d.ts +4 -0
  57. package/dist/error-resolution.js +122 -0
  58. package/dist/errors.d.ts +33 -0
  59. package/dist/errors.js +40 -0
  60. package/dist/fixture-sanitization.d.ts +28 -0
  61. package/dist/fixture-sanitization.js +227 -0
  62. package/dist/health-scenario.d.ts +1842 -0
  63. package/dist/health-scenario.js +624 -0
  64. package/dist/index.d.ts +21 -10
  65. package/dist/index.js +12 -7
  66. package/dist/lint.d.ts +23 -11
  67. package/dist/lint.js +572 -69
  68. package/dist/native-address.d.ts +43 -0
  69. package/dist/native-address.js +281 -0
  70. package/dist/native-egress-policy.d.ts +31 -0
  71. package/dist/native-egress-policy.js +288 -0
  72. package/dist/observability.d.ts +5 -2
  73. package/dist/observability.js +48 -1
  74. package/dist/provider.d.ts +10 -2
  75. package/dist/provider.js +4 -1
  76. package/dist/runtime/auth-flow.d.ts +5 -1
  77. package/dist/runtime/auth-flow.js +6 -0
  78. package/dist/runtime/browser.d.ts +1 -0
  79. package/dist/runtime/browser.js +492 -49
  80. package/dist/runtime/cache.d.ts +1 -0
  81. package/dist/runtime/cache.js +169 -15
  82. package/dist/runtime/choice-wordlist.d.ts +9 -0
  83. package/dist/runtime/choice-wordlist.js +138 -0
  84. package/dist/runtime/choice.d.ts +13 -1
  85. package/dist/runtime/choice.js +490 -102
  86. package/dist/runtime/chrome149-header-order.d.ts +58 -0
  87. package/dist/runtime/chrome149-header-order.js +289 -0
  88. package/dist/runtime/env.js +12 -0
  89. package/dist/runtime/executor.d.ts +3 -2
  90. package/dist/runtime/executor.js +26 -25
  91. package/dist/runtime/http.d.ts +1 -0
  92. package/dist/runtime/http.js +515 -53
  93. package/dist/runtime/insights.js +2 -2
  94. package/dist/runtime/instrumentation.d.ts +2 -2
  95. package/dist/runtime/instrumentation.js +366 -8
  96. package/dist/runtime/native-network-errors.d.ts +33 -0
  97. package/dist/runtime/native-network-errors.js +69 -0
  98. package/dist/runtime/native-network.d.ts +96 -0
  99. package/dist/runtime/native-network.js +1232 -0
  100. package/dist/runtime/ocr.d.ts +29 -0
  101. package/dist/runtime/ocr.js +440 -0
  102. package/dist/runtime/otlp.d.ts +71 -2
  103. package/dist/runtime/otlp.js +397 -16
  104. package/dist/runtime/proxy-errors.js +6 -2
  105. package/dist/runtime/proxy-nodemaven.d.ts +56 -0
  106. package/dist/runtime/proxy-nodemaven.js +146 -0
  107. package/dist/runtime/proxy-telemetry.d.ts +80 -1
  108. package/dist/runtime/proxy-telemetry.js +154 -47
  109. package/dist/runtime/redirects.d.ts +29 -0
  110. package/dist/runtime/redirects.js +36 -0
  111. package/dist/runtime/redis.d.ts +1 -1
  112. package/dist/runtime/redis.js +4 -2
  113. package/dist/runtime/request-options.d.ts +68 -1
  114. package/dist/runtime/request-options.js +548 -0
  115. package/dist/runtime/resolver-config.d.ts +6 -0
  116. package/dist/runtime/resolver-config.js +6 -0
  117. package/dist/runtime/resolver-public.d.ts +1 -0
  118. package/dist/runtime/resolver-public.js +1 -0
  119. package/dist/runtime/resolver-shared.d.ts +14 -0
  120. package/dist/runtime/resolver-shared.js +12 -0
  121. package/dist/runtime/resolver-vendors/bindings.d.ts +48 -0
  122. package/dist/runtime/resolver-vendors/bindings.js +40 -0
  123. package/dist/runtime/resolver-vendors/browser.d.ts +22 -0
  124. package/dist/runtime/resolver-vendors/browser.js +377 -0
  125. package/dist/runtime/resolver-vendors/capsolver.d.ts +22 -0
  126. package/dist/runtime/resolver-vendors/capsolver.js +531 -0
  127. package/dist/runtime/resolver-vendors/hosts.d.ts +2 -0
  128. package/dist/runtime/resolver-vendors/hosts.js +33 -0
  129. package/dist/runtime/resolver-vendors/twocaptcha.d.ts +24 -0
  130. package/dist/runtime/resolver-vendors/twocaptcha.js +408 -0
  131. package/dist/runtime/resolver-vendors/types.d.ts +94 -0
  132. package/dist/runtime/resolver-vendors/types.js +96 -0
  133. package/dist/runtime/resolver.d.ts +72 -0
  134. package/dist/runtime/resolver.js +771 -0
  135. package/dist/runtime/secrets.d.ts +27 -0
  136. package/dist/runtime/secrets.js +51 -0
  137. package/dist/runtime/state.d.ts +3 -0
  138. package/dist/runtime/state.js +277 -71
  139. package/dist/runtime/stealth-cookies.d.ts +20 -0
  140. package/dist/runtime/stealth-cookies.js +111 -0
  141. package/dist/runtime/stealth.d.ts +41 -5
  142. package/dist/runtime/stealth.js +997 -291
  143. package/dist/runtime/stt.js +1 -12
  144. package/dist/runtime/timeout.d.ts +5 -0
  145. package/dist/runtime/timeout.js +12 -0
  146. package/dist/runtime/trace-config.d.ts +12 -0
  147. package/dist/runtime/trace-config.js +62 -0
  148. package/dist/runtime/trace.d.ts +5 -0
  149. package/dist/runtime/trace.js +43 -10
  150. package/dist/serve.d.ts +1 -1
  151. package/dist/serve.js +1 -1
  152. package/dist/server/error-observability.d.ts +1 -0
  153. package/dist/server/error-observability.js +1 -0
  154. package/dist/server/index.d.ts +5 -2
  155. package/dist/server/index.js +2 -2
  156. package/dist/server/self-test-input-tokens.d.ts +2 -1
  157. package/dist/server/self-test-input-tokens.js +18 -14
  158. package/dist/server/self-test.d.ts +14 -3
  159. package/dist/server/self-test.js +129 -58
  160. package/dist/server/serve-implementation.d.ts +230 -0
  161. package/dist/server/serve-implementation.js +2265 -0
  162. package/dist/server/serve.d.ts +1 -70
  163. package/dist/server/serve.js +1 -1131
  164. package/dist/server/trace-output.d.ts +6 -0
  165. package/dist/server/trace-output.js +79 -0
  166. package/dist/server/types.d.ts +30 -5
  167. package/dist/server/types.js +13 -1
  168. package/dist/stateful/errors.d.ts +19 -0
  169. package/dist/stateful/errors.js +24 -0
  170. package/dist/stateful/http-provider-event-emitter.d.ts +40 -0
  171. package/dist/stateful/http-provider-event-emitter.js +237 -0
  172. package/dist/stateful/http-session-owner-registry.d.ts +44 -0
  173. package/dist/stateful/http-session-owner-registry.js +210 -0
  174. package/dist/stateful/index.d.ts +18 -0
  175. package/dist/stateful/index.js +18 -0
  176. package/dist/stateful/provider-event-delivery-failures.d.ts +32 -0
  177. package/dist/stateful/provider-event-delivery-failures.js +43 -0
  178. package/dist/stateful/provider-event-pipeline-metrics.d.ts +46 -0
  179. package/dist/stateful/provider-event-pipeline-metrics.js +48 -0
  180. package/dist/stateful/provider-event-pipeline.d.ts +50 -0
  181. package/dist/stateful/provider-event-pipeline.js +1 -0
  182. package/dist/stateful/provider-events.d.ts +101 -0
  183. package/dist/stateful/provider-events.js +289 -0
  184. package/dist/stateful/session-key.d.ts +15 -0
  185. package/dist/stateful/session-key.js +86 -0
  186. package/dist/stateful/stateful-provider-adapter-context.d.ts +5 -0
  187. package/dist/stateful/stateful-provider-adapter-context.js +42 -0
  188. package/dist/stateful/stateful-provider-adapter-metrics.d.ts +15 -0
  189. package/dist/stateful/stateful-provider-adapter-metrics.js +21 -0
  190. package/dist/stateful/stateful-provider-adapter.d.ts +98 -0
  191. package/dist/stateful/stateful-provider-adapter.js +287 -0
  192. package/dist/stateful/stateful-provider-observability.d.ts +62 -0
  193. package/dist/stateful/stateful-provider-observability.js +161 -0
  194. package/dist/stateful/stateful-provider-owner-forwarder.d.ts +41 -0
  195. package/dist/stateful/stateful-provider-owner-forwarder.js +215 -0
  196. package/dist/stateful/stateful-provider-runtime-context.d.ts +32 -0
  197. package/dist/stateful/stateful-provider-runtime-context.js +60 -0
  198. package/dist/stateful/stateful-provider-runtime-executor.d.ts +34 -0
  199. package/dist/stateful/stateful-provider-runtime-executor.js +52 -0
  200. package/dist/stateful/stateful-provider-session-routing.d.ts +67 -0
  201. package/dist/stateful/stateful-provider-session-routing.js +345 -0
  202. package/dist/stateful/stateful-provider-session-runtime.d.ts +98 -0
  203. package/dist/stateful/stateful-provider-session-runtime.js +245 -0
  204. package/dist/stateful-signing.d.ts +18 -0
  205. package/dist/stateful-signing.js +27 -0
  206. package/dist/stealth/profiles.d.ts +9 -4
  207. package/dist/stealth/profiles.js +160 -211
  208. package/dist/stream-evidence.d.ts +74 -0
  209. package/dist/stream-evidence.js +785 -0
  210. package/dist/stream.js +7 -1
  211. package/dist/testing/index.d.ts +2 -1
  212. package/dist/testing/index.js +2 -1
  213. package/dist/testing/run.d.ts +32 -2
  214. package/dist/testing/run.js +489 -21
  215. package/dist/trace-sanitization.d.ts +5 -0
  216. package/dist/trace-sanitization.js +45 -0
  217. package/dist/types.d.ts +673 -131
  218. package/dist/types.js +1 -0
  219. package/package.json +45 -5
  220. package/src/auth-turn/index.ts +1 -1
  221. package/src/auth.ts +118 -0
  222. package/src/ceremonies/index.ts +189 -46
  223. package/src/cli/__tests__/fixtures/migrate-operation-declaration/approval-override.ts.txt +6 -0
  224. package/src/cli/__tests__/fixtures/migrate-operation-declaration/codemod-syntax.ts.txt +3 -0
  225. package/src/cli/__tests__/fixtures/migrate-operation-declaration/connection-precedence.ts.txt +10 -0
  226. package/src/cli/__tests__/fixtures/migrate-operation-declaration/docs-conflict.ts.txt +8 -0
  227. package/src/cli/__tests__/fixtures/migrate-operation-declaration/examples-map.ts.txt +5 -0
  228. package/src/cli/__tests__/fixtures/migrate-operation-declaration/examples-operation.ts.txt +16 -0
  229. package/src/cli/__tests__/fixtures/migrate-operation-declaration/factory-map.ts.txt +3 -0
  230. package/src/cli/__tests__/fixtures/migrate-operation-declaration/hoist-all.ts.txt +31 -0
  231. package/src/cli/__tests__/fixtures/migrate-operation-declaration/hoisted-const.ts.txt +11 -0
  232. package/src/cli/__tests__/fixtures/migrate-operation-declaration/imported-spread.ts.txt +11 -0
  233. package/src/cli/__tests__/fixtures/migrate-operation-declaration/inline-map.ts.txt +11 -0
  234. package/src/cli/__tests__/fixtures/migrate-operation-declaration/inline-spread-cast-tail.ts.txt +21 -0
  235. package/src/cli/__tests__/fixtures/migrate-operation-declaration/inline-spread-ekitan.ts.txt +11 -0
  236. package/src/cli/__tests__/fixtures/migrate-operation-declaration/inline-spread-override.ts.txt +14 -0
  237. package/src/cli/__tests__/fixtures/migrate-operation-declaration/missing-english-locale.ts.txt +7 -0
  238. package/src/cli/__tests__/fixtures/migrate-operation-declaration/no-safety.ts.txt +6 -0
  239. package/src/cli/__tests__/fixtures/migrate-operation-declaration/non-literal.ts.txt +7 -0
  240. package/src/cli/__tests__/fixtures/migrate-operation-declaration/redundant-approval.ts.txt +6 -0
  241. package/src/cli/__tests__/fixtures/migrate-operation-declaration/safety-conflict.ts.txt +7 -0
  242. package/src/cli/__tests__/fixtures/migrate-operation-declaration/stream.ts.txt +7 -0
  243. package/src/cli/__tests__/fixtures/migrate-operation-declaration/tool-router-spread.ts.txt +15 -0
  244. package/src/cli/__tests__/fixtures/migrate-operation-declaration/unparseable.ts.txt +4 -0
  245. package/src/cli/__tests__/fixtures/migrate-operation-declaration/verbatim-template.ts.txt +12 -0
  246. package/src/cli/commands.ts +33 -0
  247. package/src/cli/create.ts +50 -37
  248. package/src/cli/migrate-operation-declaration.ts +1654 -0
  249. package/src/cli/migrate-operation-shape.ts +184 -0
  250. package/src/cli/migrate-provider-shape.ts +772 -0
  251. package/src/cli/prompt-assets.ts +865 -0
  252. package/src/cli/templates/provider/AGENTS.md.tpl +17 -8
  253. package/src/cli/templates/provider/Dockerfile.tpl +1 -1
  254. package/src/cli/templates/provider/README.md.tpl +5 -5
  255. package/src/cli/templates/provider/index.ts.tpl +6 -3
  256. package/src/cli/templates/provider/operations/ping.ts.tpl +4 -1
  257. package/src/cli/templates/provider/provider.json.tpl +6 -0
  258. package/src/config/loader.ts +690 -163
  259. package/src/contract-serialization.ts +5 -7
  260. package/src/contract-types.ts +12 -5
  261. package/src/contract.ts +24 -10
  262. package/src/declaration-validation.ts +330 -0
  263. package/src/define.ts +1048 -131
  264. package/src/dev.ts +4 -1
  265. package/src/engine.ts +279 -0
  266. package/src/error-observability.ts +64 -0
  267. package/src/error-resolution.ts +127 -0
  268. package/src/errors.ts +68 -0
  269. package/src/fixture-sanitization.ts +264 -0
  270. package/src/health-scenario.ts +875 -0
  271. package/src/index.ts +220 -13
  272. package/src/lint.ts +698 -90
  273. package/src/native-address.ts +340 -0
  274. package/src/native-egress-policy.ts +358 -0
  275. package/src/observability.ts +51 -1
  276. package/src/provider.ts +161 -3
  277. package/src/runtime/auth-flow.ts +12 -0
  278. package/src/runtime/browser.ts +661 -63
  279. package/src/runtime/cache.ts +189 -14
  280. package/src/runtime/choice-wordlist.ts +145 -0
  281. package/src/runtime/choice.ts +631 -120
  282. package/src/runtime/chrome149-header-order.ts +330 -0
  283. package/src/runtime/env.ts +13 -0
  284. package/src/runtime/executor.ts +43 -31
  285. package/src/runtime/http.ts +641 -61
  286. package/src/runtime/insights.ts +2 -2
  287. package/src/runtime/instrumentation.ts +520 -15
  288. package/src/runtime/native-network-errors.ts +99 -0
  289. package/src/runtime/native-network.ts +1605 -0
  290. package/src/runtime/ocr.ts +523 -0
  291. package/src/runtime/otlp.ts +467 -21
  292. package/src/runtime/proxy-errors.ts +12 -4
  293. package/src/runtime/proxy-nodemaven.ts +221 -0
  294. package/src/runtime/proxy-telemetry.ts +244 -75
  295. package/src/runtime/redirects.ts +66 -0
  296. package/src/runtime/redis.ts +7 -2
  297. package/src/runtime/request-options.ts +680 -1
  298. package/src/runtime/resolver-config.ts +6 -0
  299. package/src/runtime/resolver-public.ts +23 -0
  300. package/src/runtime/resolver-shared.ts +33 -0
  301. package/src/runtime/resolver-vendors/bindings.ts +56 -0
  302. package/src/runtime/resolver-vendors/browser.ts +533 -0
  303. package/src/runtime/resolver-vendors/capsolver.ts +707 -0
  304. package/src/runtime/resolver-vendors/hosts.ts +38 -0
  305. package/src/runtime/resolver-vendors/twocaptcha.ts +540 -0
  306. package/src/runtime/resolver-vendors/types.ts +212 -0
  307. package/src/runtime/resolver.ts +1157 -0
  308. package/src/runtime/secrets.ts +64 -0
  309. package/src/runtime/state.ts +394 -77
  310. package/src/runtime/stealth-cookies.ts +132 -0
  311. package/src/runtime/stealth.ts +1280 -336
  312. package/src/runtime/stt.ts +1 -19
  313. package/src/runtime/timeout.ts +18 -0
  314. package/src/runtime/trace-config.ts +78 -0
  315. package/src/runtime/trace.ts +57 -17
  316. package/src/serve.ts +6 -1
  317. package/src/server/error-observability.ts +1 -0
  318. package/src/server/index.ts +34 -2
  319. package/src/server/self-test-input-tokens.ts +29 -14
  320. package/src/server/self-test.ts +191 -68
  321. package/src/server/serve-implementation.ts +3378 -0
  322. package/src/server/serve.ts +1 -1632
  323. package/src/server/trace-output.ts +129 -0
  324. package/src/server/types.ts +13 -1
  325. package/src/stateful/README.md +146 -0
  326. package/src/stateful/errors.ts +35 -0
  327. package/src/stateful/http-provider-event-emitter.ts +314 -0
  328. package/src/stateful/http-session-owner-registry.ts +306 -0
  329. package/src/stateful/index.ts +18 -0
  330. package/src/stateful/provider-event-delivery-failures.ts +80 -0
  331. package/src/stateful/provider-event-pipeline-metrics.ts +95 -0
  332. package/src/stateful/provider-event-pipeline.ts +61 -0
  333. package/src/stateful/provider-events.ts +462 -0
  334. package/src/stateful/session-key.ts +111 -0
  335. package/src/stateful/stateful-provider-adapter-context.ts +59 -0
  336. package/src/stateful/stateful-provider-adapter-metrics.ts +48 -0
  337. package/src/stateful/stateful-provider-adapter.ts +562 -0
  338. package/src/stateful/stateful-provider-observability.ts +261 -0
  339. package/src/stateful/stateful-provider-owner-forwarder.ts +287 -0
  340. package/src/stateful/stateful-provider-runtime-context.ts +92 -0
  341. package/src/stateful/stateful-provider-runtime-executor.ts +96 -0
  342. package/src/stateful/stateful-provider-session-routing.ts +546 -0
  343. package/src/stateful/stateful-provider-session-runtime.ts +403 -0
  344. package/src/stateful-signing.ts +46 -0
  345. package/src/stealth/profiles.ts +202 -231
  346. package/src/stream-evidence.ts +988 -0
  347. package/src/stream.ts +8 -1
  348. package/src/testing/index.ts +10 -1
  349. package/src/testing/run.ts +658 -15
  350. package/src/trace-sanitization.ts +63 -0
  351. package/src/types.ts +788 -174
  352. package/dist/cli/templates/provider/CLAUDE.md.tpl +0 -1
  353. package/src/cli/templates/provider/CLAUDE.md.tpl +0 -1
  354. /package/dist/cli/templates/provider/{skills → .agents/skills}/fixtures-and-recording/SKILL.md.tpl +0 -0
  355. /package/dist/cli/templates/provider/{skills → .agents/skills}/health-checks-and-fail-closed/SKILL.md.tpl +0 -0
  356. /package/dist/cli/templates/provider/{skills → .agents/skills}/normalization-standards/SKILL.md.tpl +0 -0
  357. /package/dist/cli/templates/provider/{skills → .agents/skills}/pagination-and-counts/SKILL.md.tpl +0 -0
  358. /package/dist/cli/templates/provider/{skills → .agents/skills}/upstream-contract-verification/SKILL.md.tpl +0 -0
  359. /package/dist/cli/templates/provider/{skills → .agents/skills}/upstream-notes/README.md.tpl +0 -0
  360. /package/src/cli/templates/provider/{skills → .agents/skills}/fixtures-and-recording/SKILL.md.tpl +0 -0
  361. /package/src/cli/templates/provider/{skills → .agents/skills}/health-checks-and-fail-closed/SKILL.md.tpl +0 -0
  362. /package/src/cli/templates/provider/{skills → .agents/skills}/normalization-standards/SKILL.md.tpl +0 -0
  363. /package/src/cli/templates/provider/{skills → .agents/skills}/pagination-and-counts/SKILL.md.tpl +0 -0
  364. /package/src/cli/templates/provider/{skills → .agents/skills}/upstream-contract-verification/SKILL.md.tpl +0 -0
  365. /package/src/cli/templates/provider/{skills → .agents/skills}/upstream-notes/README.md.tpl +0 -0
@@ -4,6 +4,7 @@ import {
4
4
  createHash,
5
5
  createHmac,
6
6
  randomBytes,
7
+ randomInt,
7
8
  timingSafeEqual,
8
9
  } from "node:crypto";
9
10
  import {
@@ -11,18 +12,29 @@ import {
11
12
  ProviderChoiceTokenError,
12
13
  type ProviderChoiceTokenPayload,
13
14
  } from "../choice-token.js";
14
- import { ProviderError } from "../errors.js";
15
+ import { isProviderError, ProviderError } from "../errors.js";
16
+ import {
17
+ CHOICE_WORDLIST_SIZE,
18
+ choiceWordAt,
19
+ HIGH_CHOICE_WORD_COUNT,
20
+ isChoiceWord,
21
+ STANDARD_CHOICE_WORD_COUNT,
22
+ } from "./choice-wordlist.js";
15
23
  import type {
16
24
  CredentialContext,
17
25
  EnvContext,
18
26
  ProviderChoiceBindingOptions,
27
+ ProviderChoiceConsumeMode,
28
+ ProviderChoiceConsumeResult,
19
29
  ProviderChoiceContext,
30
+ ProviderChoiceExplicitParseResult,
20
31
  ProviderChoiceIssueOptions,
21
32
  ProviderChoiceParseOptions,
22
33
  ProviderChoiceStorageOptions,
23
34
  ProviderRequestContext,
24
35
  ProviderRuntimeState,
25
36
  ProviderStateDurationString,
37
+ StateValue,
26
38
  } from "../types.js";
27
39
 
28
40
  export const PROVIDER_RUNTIME_CHOICE_TOKEN_MASTER_SECRET_ENV =
@@ -30,6 +42,9 @@ export const PROVIDER_RUNTIME_CHOICE_TOKEN_MASTER_SECRET_ENV =
30
42
 
31
43
  const PRIMARY_CHOICE_TOKEN_KID = "v1";
32
44
  const MANAGED_CHOICE_TOKEN_VERSION = 1;
45
+ const SERVER_STORED_CHOICE_RECORD_VERSION = 1;
46
+ const SERVER_STORED_CHOICE_ISSUE_ATTEMPTS = 5;
47
+ const WORD_CHOICE_NOT_FOUND_MESSAGE = "Provider choice token was not found.";
33
48
 
34
49
  type ManagedChoiceEnvelope = {
35
50
  readonly v: typeof MANAGED_CHOICE_TOKEN_VERSION;
@@ -51,6 +66,32 @@ type ServerChoiceHandlePayload = {
51
66
  readonly created_at_ms: number;
52
67
  };
53
68
 
69
+ type ServerStoredChoiceRecord = {
70
+ readonly v: typeof SERVER_STORED_CHOICE_RECORD_VERSION;
71
+ readonly storage: "server";
72
+ readonly status: "active" | "consumed";
73
+ readonly provider_id: string;
74
+ readonly purpose: string;
75
+ readonly issued_at_ms: number;
76
+ readonly ttl_ms: number;
77
+ readonly binding?: ManagedChoiceEnvelope["binding"];
78
+ readonly prefix: string;
79
+ readonly payload: ProviderChoiceTokenPayload;
80
+ readonly payload_digest: string;
81
+ readonly replay_key: string;
82
+ };
83
+
84
+ export type ProviderChoiceTelemetryEvent = {
85
+ readonly providerId: string;
86
+ readonly purpose: string;
87
+ readonly operation: "parse" | "consume";
88
+ readonly format: "word" | "legacy";
89
+ readonly outcome: "success" | "not-found" | "invalid" | "unsupported" | "error";
90
+ readonly consumeMode: ProviderChoiceConsumeMode;
91
+ readonly consumed: boolean;
92
+ readonly replay: boolean;
93
+ };
94
+
54
95
  export type CreateProviderChoiceContextOptions = {
55
96
  readonly providerId: string;
56
97
  readonly env?: EnvContext;
@@ -59,6 +100,8 @@ export type CreateProviderChoiceContextOptions = {
59
100
  readonly state?: ProviderRuntimeState;
60
101
  readonly masterSecret?: string;
61
102
  readonly kid?: string;
103
+ /** Receives allowlisted metadata only; token and payload values are never included. */
104
+ readonly onTelemetry?: (event: ProviderChoiceTelemetryEvent) => void;
62
105
  };
63
106
 
64
107
  export function createProviderChoiceContext(
@@ -86,6 +129,40 @@ export function createProviderChoiceContext(
86
129
  issueOptions: ProviderChoiceIssueOptions<TPayload>,
87
130
  ): string | Promise<string> {
88
131
  const issuedAtMs = issueOptions.nowMs ?? Date.now();
132
+ const resolvedStorage = resolveIssueStorage(issueOptions.storage, issueOptions.payload);
133
+ if (resolvedStorage.mode === "server") {
134
+ const keys = hasRequestedChoiceBinding(issueOptions.bind)
135
+ ? deriveManagedChoiceKeys({
136
+ masterSecret: resolveMasterSecret(),
137
+ providerId: options.providerId,
138
+ purpose: issueOptions.purpose,
139
+ kid,
140
+ })
141
+ : undefined;
142
+ const binding = hasRequestedChoiceBinding(issueOptions.bind)
143
+ ? createChoiceBinding({
144
+ keys: keys!,
145
+ options: issueOptions.bind,
146
+ request: options.request,
147
+ credential: options.credential,
148
+ required: true,
149
+ })
150
+ : undefined;
151
+ const baseEnvelope = {
152
+ v: MANAGED_CHOICE_TOKEN_VERSION,
153
+ provider_id: options.providerId,
154
+ purpose: issueOptions.purpose,
155
+ issued_at_ms: issuedAtMs,
156
+ ttl_ms: issueOptions.ttlMs,
157
+ binding,
158
+ } satisfies Omit<ManagedChoiceEnvelope, "payload">;
159
+ return issueWordServerStoredChoice({
160
+ baseEnvelope,
161
+ issueOptions,
162
+ storage: resolvedStorage.storage,
163
+ contextState: options.state,
164
+ });
165
+ }
89
166
  const keys = deriveManagedChoiceKeys({
90
167
  masterSecret: resolveMasterSecret(),
91
168
  providerId: options.providerId,
@@ -106,18 +183,6 @@ export function createProviderChoiceContext(
106
183
  required: true,
107
184
  }),
108
185
  };
109
- const resolvedStorage = resolveIssueStorage(issueOptions.storage, issueOptions.payload);
110
- if (resolvedStorage.mode === "server") {
111
- return issueServerStoredChoice({
112
- baseEnvelope,
113
- issueOptions,
114
- storage: resolvedStorage.storage,
115
- contextState: options.state,
116
- kid,
117
- keys,
118
- issuedAtMs,
119
- });
120
- }
121
186
  const envelope: ManagedChoiceEnvelope = {
122
187
  ...baseEnvelope,
123
188
  payload: issueOptions.payload,
@@ -130,6 +195,9 @@ export function createProviderChoiceContext(
130
195
  });
131
196
  }
132
197
 
198
+ function parse(
199
+ parseOptions: ProviderChoiceParseOptions & { readonly consume: "explicit" },
200
+ ): Promise<ProviderChoiceExplicitParseResult>;
133
201
  function parse(
134
202
  parseOptions: ProviderChoiceParseOptions & {
135
203
  readonly storage?: { readonly mode: "inline" };
@@ -147,68 +215,154 @@ export function createProviderChoiceContext(
147
215
  ): ProviderChoiceTokenPayload | Promise<ProviderChoiceTokenPayload>;
148
216
  function parse(
149
217
  parseOptions: ProviderChoiceParseOptions,
150
- ): ProviderChoiceTokenPayload | Promise<ProviderChoiceTokenPayload> {
151
- const [actualPrefix, tokenKid, encodedIv, encryptedPayload, authTag, signature] =
152
- parseManagedChoiceTokenParts(parseOptions.token);
153
- if (
154
- actualPrefix !== parseOptions.prefix ||
155
- tokenKid !== kid ||
156
- !encodedIv ||
157
- !encryptedPayload ||
158
- !authTag ||
159
- !signature
160
- ) {
161
- throw new ProviderChoiceTokenError(
162
- "invalid_shape",
163
- "Provider choice token shape is invalid.",
164
- );
165
- }
166
-
167
- const keys = deriveManagedChoiceKeys({
168
- masterSecret: resolveMasterSecret(),
169
- providerId: options.providerId,
170
- purpose: parseOptions.purpose,
171
- kid: tokenKid,
172
- });
173
- const signedBody = [parseOptions.prefix, tokenKid, encodedIv, encryptedPayload, authTag].join(
174
- ".",
175
- );
176
- assertManagedChoiceSignature({
177
- signedBody,
178
- signature,
179
- signingKey: keys.signing,
180
- });
181
- const envelope = decryptManagedChoiceToken({
182
- encodedIv,
183
- encryptedPayload,
184
- authTag,
185
- encryptionKey: keys.encryption,
186
- });
187
- assertManagedChoiceEnvelope(envelope, {
188
- providerId: options.providerId,
189
- purpose: parseOptions.purpose,
190
- ttlMs: parseOptions.ttlMs,
191
- nowMs: parseOptions.nowMs,
192
- futureToleranceMs: parseOptions.futureToleranceMs,
218
+ ):
219
+ | ProviderChoiceTokenPayload
220
+ | ProviderChoiceExplicitParseResult
221
+ | Promise<ProviderChoiceTokenPayload | ProviderChoiceExplicitParseResult> {
222
+ const consumeMode = parseOptions.consume ?? "never";
223
+ const wordStateKey = parseWordChoiceStateKey({
224
+ token: parseOptions.token,
225
+ prefix: parseOptions.prefix,
193
226
  });
194
- assertChoiceBindingMatches({
195
- actual: envelope.binding,
196
- expected: createChoiceBinding({
197
- keys,
198
- options: parseOptions.bind,
227
+ if (wordStateKey) {
228
+ const parsed = parseWordServerStoredChoice({
229
+ stateKey: wordStateKey,
230
+ parseOptions,
231
+ contextState: options.state,
232
+ providerId: options.providerId,
199
233
  request: options.request,
200
234
  credential: options.credential,
201
- required: true,
202
- }),
203
- });
204
- if (isServerChoiceHandlePayload(envelope.payload)) {
205
- return parseServerStoredChoice({
206
- handle: envelope.payload,
207
- storage: parseOptions.storage,
208
- contextState: options.state,
235
+ resolveBindingKeys: () =>
236
+ deriveManagedChoiceKeys({
237
+ masterSecret: resolveMasterSecret(),
238
+ providerId: options.providerId,
239
+ purpose: parseOptions.purpose,
240
+ kid,
241
+ }),
242
+ onConsume: (result) =>
243
+ emitChoiceTelemetry(options.onTelemetry, {
244
+ providerId: options.providerId,
245
+ purpose: parseOptions.purpose,
246
+ operation: "consume",
247
+ format: "word",
248
+ outcome: "success",
249
+ consumeMode,
250
+ consumed: result.status === "consumed",
251
+ replay: result.status === "already-consumed",
252
+ }),
209
253
  });
254
+ return observeChoiceParse<ProviderChoiceTokenPayload | ProviderChoiceExplicitParseResult>(
255
+ parsed,
256
+ {
257
+ onTelemetry: options.onTelemetry,
258
+ providerId: options.providerId,
259
+ purpose: parseOptions.purpose,
260
+ format: "word",
261
+ consumeMode,
262
+ },
263
+ );
264
+ }
265
+
266
+ // Inline choices continue to use the encrypted envelope. A structurally
267
+ // valid word token returns above, so lookup, expiry, consumption, and
268
+ // binding failures can never enter this branch.
269
+ try {
270
+ const [actualPrefix, tokenKid, encodedIv, encryptedPayload, authTag, signature] =
271
+ parseManagedChoiceTokenParts(parseOptions.token);
272
+ if (
273
+ actualPrefix !== parseOptions.prefix ||
274
+ tokenKid !== kid ||
275
+ !encodedIv ||
276
+ !encryptedPayload ||
277
+ !authTag ||
278
+ !signature
279
+ ) {
280
+ throw new ProviderChoiceTokenError(
281
+ "invalid_shape",
282
+ "Provider choice token shape is invalid.",
283
+ );
284
+ }
285
+
286
+ const keys = deriveManagedChoiceKeys({
287
+ masterSecret: resolveMasterSecret(),
288
+ providerId: options.providerId,
289
+ purpose: parseOptions.purpose,
290
+ kid: tokenKid,
291
+ });
292
+ const signedBody = [parseOptions.prefix, tokenKid, encodedIv, encryptedPayload, authTag].join(
293
+ ".",
294
+ );
295
+ assertManagedChoiceSignature({
296
+ signedBody,
297
+ signature,
298
+ signingKey: keys.signing,
299
+ });
300
+ const envelope = decryptManagedChoiceToken({
301
+ encodedIv,
302
+ encryptedPayload,
303
+ authTag,
304
+ encryptionKey: keys.encryption,
305
+ });
306
+ assertManagedChoiceEnvelope(envelope, {
307
+ providerId: options.providerId,
308
+ purpose: parseOptions.purpose,
309
+ ttlMs: parseOptions.ttlMs,
310
+ nowMs: parseOptions.nowMs,
311
+ futureToleranceMs: parseOptions.futureToleranceMs,
312
+ });
313
+ assertChoiceBindingMatches({
314
+ actual: envelope.binding,
315
+ expected: createChoiceBinding({
316
+ keys,
317
+ options: parseOptions.bind,
318
+ request: options.request,
319
+ credential: options.credential,
320
+ required: true,
321
+ }),
322
+ });
323
+ if (isServerChoiceHandlePayload(envelope.payload)) {
324
+ throw wordChoiceNotFoundError();
325
+ }
326
+ const payload = envelope.payload;
327
+ const parsed =
328
+ consumeMode === "explicit"
329
+ ? Promise.resolve(payload).then((resolvedPayload) =>
330
+ createInlineExplicitParseResult({
331
+ payload: resolvedPayload,
332
+ replayKey: digestChoiceReplayKey(parseOptions.token),
333
+ onConsume: () =>
334
+ emitChoiceTelemetry(options.onTelemetry, {
335
+ providerId: options.providerId,
336
+ purpose: parseOptions.purpose,
337
+ operation: "consume",
338
+ format: "legacy",
339
+ outcome: "unsupported",
340
+ consumeMode,
341
+ consumed: false,
342
+ replay: false,
343
+ }),
344
+ }),
345
+ )
346
+ : payload;
347
+ return observeChoiceParse<ProviderChoiceTokenPayload | ProviderChoiceExplicitParseResult>(
348
+ parsed,
349
+ {
350
+ onTelemetry: options.onTelemetry,
351
+ providerId: options.providerId,
352
+ purpose: parseOptions.purpose,
353
+ format: "legacy",
354
+ consumeMode,
355
+ },
356
+ );
357
+ } catch (error) {
358
+ emitChoiceParseFailure(options.onTelemetry, error, {
359
+ providerId: options.providerId,
360
+ purpose: parseOptions.purpose,
361
+ format: "legacy",
362
+ consumeMode,
363
+ });
364
+ throw error;
210
365
  }
211
- return envelope.payload;
212
366
  }
213
367
 
214
368
  return { issue, parse };
@@ -226,6 +380,109 @@ export function createTestProviderChoiceContext(
226
380
  });
227
381
  }
228
382
 
383
+ type ChoiceParseTelemetryBase = {
384
+ readonly onTelemetry?: (event: ProviderChoiceTelemetryEvent) => void;
385
+ readonly providerId: string;
386
+ readonly purpose: string;
387
+ readonly format: "word" | "legacy";
388
+ readonly consumeMode: ProviderChoiceConsumeMode;
389
+ };
390
+
391
+ function observeChoiceParse<T>(
392
+ result: T | Promise<T>,
393
+ base: ChoiceParseTelemetryBase,
394
+ ): T | Promise<T> {
395
+ if (result instanceof Promise) {
396
+ return result.then(
397
+ (value) => {
398
+ emitChoiceParseSuccess(base, value);
399
+ return value;
400
+ },
401
+ (error: unknown) => {
402
+ emitChoiceParseFailure(base.onTelemetry, error, base);
403
+ throw error;
404
+ },
405
+ );
406
+ }
407
+ emitChoiceParseSuccess(base, result);
408
+ return result;
409
+ }
410
+
411
+ function emitChoiceParseSuccess(base: ChoiceParseTelemetryBase, result: unknown): void {
412
+ const replay = isConsumedChoiceReplay(result);
413
+ emitChoiceTelemetry(base.onTelemetry, {
414
+ providerId: base.providerId,
415
+ purpose: base.purpose,
416
+ operation: "parse",
417
+ format: base.format,
418
+ outcome: "success",
419
+ consumeMode: base.consumeMode,
420
+ consumed: replay || (base.format === "word" && base.consumeMode === "on-parse"),
421
+ replay,
422
+ });
423
+ }
424
+
425
+ function isConsumedChoiceReplay(value: unknown): boolean {
426
+ return (
427
+ value !== null &&
428
+ typeof value === "object" &&
429
+ "status" in value &&
430
+ value.status === "consumed" &&
431
+ "replayKey" in value &&
432
+ typeof value.replayKey === "string"
433
+ );
434
+ }
435
+
436
+ function emitChoiceParseFailure(
437
+ onTelemetry: CreateProviderChoiceContextOptions["onTelemetry"],
438
+ error: unknown,
439
+ base: Omit<ChoiceParseTelemetryBase, "onTelemetry">,
440
+ ): void {
441
+ const outcome =
442
+ error instanceof ProviderChoiceTokenError
443
+ ? base.format === "word" && error.message === WORD_CHOICE_NOT_FOUND_MESSAGE
444
+ ? "not-found"
445
+ : "invalid"
446
+ : "error";
447
+ emitChoiceTelemetry(onTelemetry, {
448
+ providerId: base.providerId,
449
+ purpose: base.purpose,
450
+ operation: "parse",
451
+ format: base.format,
452
+ outcome,
453
+ consumeMode: base.consumeMode,
454
+ consumed: false,
455
+ replay: false,
456
+ });
457
+ }
458
+
459
+ function emitChoiceTelemetry(
460
+ onTelemetry: CreateProviderChoiceContextOptions["onTelemetry"],
461
+ event: ProviderChoiceTelemetryEvent,
462
+ ): void {
463
+ try {
464
+ onTelemetry?.(event);
465
+ } catch {
466
+ // Observability must never change provider token semantics.
467
+ }
468
+ }
469
+
470
+ function createInlineExplicitParseResult(options: {
471
+ readonly payload: ProviderChoiceTokenPayload;
472
+ readonly replayKey: string;
473
+ readonly onConsume: () => void;
474
+ }): ProviderChoiceExplicitParseResult {
475
+ return {
476
+ status: "active",
477
+ payload: options.payload,
478
+ replayKey: options.replayKey,
479
+ consume: async () => {
480
+ options.onConsume();
481
+ return { status: "unsupported" };
482
+ },
483
+ };
484
+ }
485
+
229
486
  function resolveChoiceMasterSecret(options: CreateProviderChoiceContextOptions): string {
230
487
  const configured =
231
488
  options.masterSecret ?? options.env?.get(PROVIDER_RUNTIME_CHOICE_TOKEN_MASTER_SECRET_ENV);
@@ -300,80 +557,296 @@ function encryptManagedChoiceToken(options: {
300
557
  return `${signedBody}.${signature}`;
301
558
  }
302
559
 
303
- async function issueServerStoredChoice<TPayload extends ProviderChoiceTokenPayload>(options: {
560
+ async function issueWordServerStoredChoice<TPayload extends ProviderChoiceTokenPayload>(options: {
304
561
  readonly baseEnvelope: Omit<ManagedChoiceEnvelope, "payload">;
305
562
  readonly issueOptions: ProviderChoiceIssueOptions<TPayload>;
306
563
  readonly storage: ServerProviderChoiceStorageOptions;
307
564
  readonly contextState?: ProviderRuntimeState;
308
- readonly kid: string;
309
- readonly keys: ManagedChoiceKeys;
310
- readonly issuedAtMs: number;
311
565
  }): Promise<string> {
312
566
  const serializedPayload = serializeChoicePayload(options.issueOptions.payload);
313
- const payloadBytes = Buffer.byteLength(serializedPayload, "utf8");
314
- if (payloadBytes > options.storage.maxValueBytes) {
315
- throw new ProviderError("Provider choice payload exceeds state storage policy.", {
316
- code: "CHOICE_STATE_PAYLOAD_TOO_LARGE",
317
- category: "input_validation",
318
- retryable: false,
319
- details: {
320
- maxValueBytes: options.storage.maxValueBytes,
321
- payloadBytes,
322
- },
323
- });
324
- }
325
- const stateId = `choice_${randomBytes(16).toString("base64url")}`;
326
- const digest = digestChoicePayload(serializedPayload);
567
+ const payloadDigest = digestChoicePayload(serializedPayload);
327
568
  const namespace = resolveChoiceStateNamespace({
328
569
  storage: options.storage,
329
570
  contextState: options.contextState,
330
571
  ttlMs: options.issueOptions.ttlMs,
331
572
  });
332
- await namespace.set(optionsStateKey(stateId), options.issueOptions.payload, {
333
- ttl: stateTtl(options.storage, options.issueOptions.ttlMs),
334
- });
335
- const envelope: ManagedChoiceEnvelope = {
336
- ...options.baseEnvelope,
337
- payload: {
573
+ const wordCount =
574
+ options.issueOptions.strength === "high" ? HIGH_CHOICE_WORD_COUNT : STANDARD_CHOICE_WORD_COUNT;
575
+ for (let attempt = 0; attempt < SERVER_STORED_CHOICE_ISSUE_ATTEMPTS; attempt += 1) {
576
+ const stateKey = generateChoiceWordSequence(wordCount);
577
+ const token = `${options.issueOptions.prefix}${stateKey}`;
578
+ const record: ServerStoredChoiceRecord = {
579
+ v: SERVER_STORED_CHOICE_RECORD_VERSION,
338
580
  storage: "server",
339
- state_id: stateId,
340
- payload_digest: digest,
341
- created_at_ms: options.issuedAtMs,
342
- },
343
- };
344
- return encryptManagedChoiceToken({
345
- prefix: options.issueOptions.prefix,
346
- kid: options.kid,
347
- envelope,
348
- keys: options.keys,
581
+ status: "active",
582
+ provider_id: options.baseEnvelope.provider_id,
583
+ purpose: options.baseEnvelope.purpose,
584
+ issued_at_ms: options.baseEnvelope.issued_at_ms,
585
+ ttl_ms: options.baseEnvelope.ttl_ms,
586
+ binding: options.baseEnvelope.binding,
587
+ prefix: options.issueOptions.prefix,
588
+ payload: options.issueOptions.payload,
589
+ payload_digest: payloadDigest,
590
+ replay_key: digestChoiceReplayKey(token),
591
+ };
592
+ const valueBytes = Buffer.byteLength(JSON.stringify(record), "utf8");
593
+ if (valueBytes > options.storage.maxValueBytes) {
594
+ throw new ProviderError("Provider choice payload exceeds state storage policy.", {
595
+ code: "CHOICE_STATE_PAYLOAD_TOO_LARGE",
596
+ category: "input_validation",
597
+ retryable: false,
598
+ details: {
599
+ maxValueBytes: options.storage.maxValueBytes,
600
+ valueBytes,
601
+ },
602
+ });
603
+ }
604
+ const result = await namespace.compareAndSet(optionsStateKey(stateKey), 0, record, {
605
+ ttl: stateTtl(options.storage, options.issueOptions.ttlMs),
606
+ });
607
+ if (result.ok) return token;
608
+ }
609
+ throw new ProviderError("Provider choice state storage is not available.", {
610
+ code: "CHOICE_STATE_UNAVAILABLE",
611
+ category: "internal_error",
612
+ retryable: false,
349
613
  });
350
614
  }
351
615
 
352
- async function parseServerStoredChoice(options: {
353
- readonly handle: ServerChoiceHandlePayload;
354
- readonly storage?: ProviderChoiceStorageOptions;
616
+ async function parseWordServerStoredChoice(options: {
617
+ readonly stateKey: string;
618
+ readonly parseOptions: ProviderChoiceParseOptions;
355
619
  readonly contextState?: ProviderRuntimeState;
356
- }): Promise<ProviderChoiceTokenPayload> {
357
- const storage = resolveParseStorage(options.storage);
620
+ readonly providerId: string;
621
+ readonly request?: ProviderRequestContext;
622
+ readonly credential?: CredentialContext;
623
+ readonly resolveBindingKeys: () => ManagedChoiceKeys;
624
+ readonly onConsume: (result: ProviderChoiceConsumeResult) => void;
625
+ }): Promise<ProviderChoiceTokenPayload | ProviderChoiceExplicitParseResult> {
626
+ const storage = resolveParseStorage(options.parseOptions.storage);
358
627
  const namespace = resolveChoiceStateNamespace({
359
628
  storage,
360
629
  contextState: options.contextState,
630
+ ttlMs: options.parseOptions.ttlMs,
361
631
  });
362
- const record = await namespace.get<ProviderChoiceTokenPayload>(
363
- optionsStateKey(options.handle.state_id),
632
+ let stored: StateValue<ServerStoredChoiceRecord> | null;
633
+ try {
634
+ stored = await namespace.get<ServerStoredChoiceRecord>(optionsStateKey(options.stateKey));
635
+ } catch (error) {
636
+ if (isProviderError(error)) throw error;
637
+ throw wordChoiceNotFoundError();
638
+ }
639
+ if (!stored || !isServerStoredChoiceRecord(stored.value)) {
640
+ throw wordChoiceNotFoundError();
641
+ }
642
+
643
+ const record = stored.value;
644
+ const expectedReplayKey = digestChoiceReplayKey(
645
+ `${options.parseOptions.prefix}${options.stateKey}`,
364
646
  );
365
- if (!record) {
366
- throw new ProviderChoiceTokenError(
367
- "invalid_payload",
368
- "Provider choice token state payload is missing.",
647
+ try {
648
+ if (
649
+ record.provider_id !== options.providerId ||
650
+ record.purpose !== options.parseOptions.purpose ||
651
+ record.prefix !== options.parseOptions.prefix
652
+ ) {
653
+ throw wordChoiceNotFoundError();
654
+ }
655
+ assertPayloadDigestMatches({
656
+ actual: digestChoicePayload(serializeChoicePayload(record.payload)),
657
+ expected: record.payload_digest,
658
+ });
659
+ assertPayloadDigestMatches({ actual: expectedReplayKey, expected: record.replay_key });
660
+ assertWordChoiceBindingMatches({
661
+ actual: record.binding,
662
+ requested: options.parseOptions.bind,
663
+ request: options.request,
664
+ credential: options.credential,
665
+ resolveKeys: options.resolveBindingKeys,
666
+ });
667
+ } catch (error) {
668
+ if (
669
+ error instanceof ProviderChoiceTokenError ||
670
+ (isProviderError(error) && error.code === "CHOICE_CONTEXT_REQUIRED")
671
+ ) {
672
+ throw wordChoiceNotFoundError();
673
+ }
674
+ throw error;
675
+ }
676
+ // Freshness is classified last, reachable only after every identity,
677
+ // integrity, and binding check above has passed (ADR 0006, amended
678
+ // 2026-08-20): a caller that proved the record's binding may observe the
679
+ // canonical stale error, while an unbound record keeps the collapsed
680
+ // not-found error so expiry never becomes an existence signal for
681
+ // guessable tokens.
682
+ try {
683
+ assertFreshProviderChoiceIssuedAt(record.issued_at_ms, {
684
+ ttlMs:
685
+ options.parseOptions.ttlMs != null
686
+ ? Math.min(options.parseOptions.ttlMs, record.ttl_ms)
687
+ : record.ttl_ms,
688
+ nowMs: options.parseOptions.nowMs,
689
+ futureToleranceMs: options.parseOptions.futureToleranceMs,
690
+ });
691
+ } catch (error) {
692
+ const recordIsBound = Boolean(
693
+ record.binding?.connection_hash || record.binding?.credential_hash,
369
694
  );
695
+ if (recordIsBound && error instanceof ProviderChoiceTokenError && error.reason === "stale") {
696
+ throw error;
697
+ }
698
+ if (error instanceof ProviderChoiceTokenError) throw wordChoiceNotFoundError();
699
+ throw error;
700
+ }
701
+
702
+ const consumeMode = options.parseOptions.consume ?? "never";
703
+ if (record.status === "consumed") {
704
+ if (consumeMode === "explicit") {
705
+ return { status: "consumed", replayKey: record.replay_key };
706
+ }
707
+ throw wordChoiceNotFoundError();
708
+ }
709
+ if (consumeMode === "never") return record.payload;
710
+ if (consumeMode === "explicit") {
711
+ return {
712
+ status: "active",
713
+ payload: record.payload,
714
+ replayKey: record.replay_key,
715
+ consume: async () => {
716
+ const result = await consumeWordServerStoredChoice({
717
+ stateKey: options.stateKey,
718
+ stored,
719
+ record,
720
+ storage,
721
+ contextState: options.contextState,
722
+ });
723
+ options.onConsume(result);
724
+ return result;
725
+ },
726
+ };
370
727
  }
371
- const serializedPayload = serializeChoicePayload(record.value);
372
- assertPayloadDigestMatches({
373
- actual: digestChoicePayload(serializedPayload),
374
- expected: options.handle.payload_digest,
728
+ const consumed = await consumeWordServerStoredChoice({
729
+ stateKey: options.stateKey,
730
+ stored,
731
+ record,
732
+ storage,
733
+ contextState: options.contextState,
375
734
  });
376
- return record.value;
735
+ if (consumed.status !== "consumed") throw wordChoiceNotFoundError();
736
+ return record.payload;
737
+ }
738
+
739
+ async function consumeWordServerStoredChoice(options: {
740
+ readonly stateKey: string;
741
+ readonly stored: StateValue<ServerStoredChoiceRecord>;
742
+ readonly record: ServerStoredChoiceRecord;
743
+ readonly storage: ServerProviderChoiceStorageOptions;
744
+ readonly contextState?: ProviderRuntimeState;
745
+ }): Promise<ProviderChoiceConsumeResult> {
746
+ const namespace = resolveChoiceStateNamespace({
747
+ storage: options.storage,
748
+ contextState: options.contextState,
749
+ ttlMs: options.record.ttl_ms,
750
+ });
751
+ try {
752
+ const consumed = await namespace.compareAndSet(
753
+ optionsStateKey(options.stateKey),
754
+ options.stored.version,
755
+ { ...options.record, status: "consumed" } satisfies ServerStoredChoiceRecord,
756
+ { ttl: remainingStateTtl(options.stored.expiresAt) },
757
+ );
758
+ if (consumed.ok) return { status: "consumed" };
759
+ if (
760
+ consumed.current &&
761
+ isServerStoredChoiceRecord(consumed.current.value) &&
762
+ consumed.current.value.status === "consumed" &&
763
+ consumed.current.value.replay_key === options.record.replay_key
764
+ ) {
765
+ return { status: "already-consumed" };
766
+ }
767
+ throw wordChoiceNotFoundError();
768
+ } catch (error) {
769
+ if (isProviderError(error)) throw error;
770
+ if (error instanceof ProviderChoiceTokenError) throw error;
771
+ throw wordChoiceNotFoundError();
772
+ }
773
+ }
774
+
775
+ function generateChoiceWordSequence(wordCount: number): string {
776
+ return Array.from({ length: wordCount }, () =>
777
+ choiceWordAt(randomInt(CHOICE_WORDLIST_SIZE)),
778
+ ).join("-");
779
+ }
780
+
781
+ function parseWordChoiceStateKey(options: {
782
+ readonly token: string;
783
+ readonly prefix: string;
784
+ }): string | null {
785
+ if (!options.token.startsWith(options.prefix)) return null;
786
+ const body = options.token.slice(options.prefix.length);
787
+ // The official list contains one hyphenated entry (`yo-yo`), so structural
788
+ // recognition uses dictionary-aware segmentation instead of assuming every
789
+ // hyphen is a word boundary.
790
+ if (!/^[a-z]+(?:-[a-z]+){3,9}$/.test(body)) return null;
791
+ const segments = body.split("-");
792
+ if (
793
+ !canSegmentChoiceWords(segments, 0, STANDARD_CHOICE_WORD_COUNT) &&
794
+ !canSegmentChoiceWords(segments, 0, HIGH_CHOICE_WORD_COUNT)
795
+ ) {
796
+ return null;
797
+ }
798
+ return body;
799
+ }
800
+
801
+ function canSegmentChoiceWords(
802
+ segments: readonly string[],
803
+ segmentIndex: number,
804
+ wordsRemaining: number,
805
+ ): boolean {
806
+ if (wordsRemaining === 0) return segmentIndex === segments.length;
807
+ const segmentsRemaining = segments.length - segmentIndex;
808
+ if (segmentsRemaining < wordsRemaining) return false;
809
+ for (let end = segmentIndex + 1; end <= segments.length - (wordsRemaining - 1); end += 1) {
810
+ const candidate = segments.slice(segmentIndex, end).join("-");
811
+ if (candidate.length > 10) break;
812
+ if (isChoiceWord(candidate) && canSegmentChoiceWords(segments, end, wordsRemaining - 1)) {
813
+ return true;
814
+ }
815
+ }
816
+ return false;
817
+ }
818
+
819
+ function isServerStoredChoiceRecord(value: unknown): value is ServerStoredChoiceRecord {
820
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
821
+ return (
822
+ "v" in value &&
823
+ value.v === SERVER_STORED_CHOICE_RECORD_VERSION &&
824
+ "storage" in value &&
825
+ value.storage === "server" &&
826
+ "status" in value &&
827
+ (value.status === "active" || value.status === "consumed") &&
828
+ "provider_id" in value &&
829
+ typeof value.provider_id === "string" &&
830
+ "purpose" in value &&
831
+ typeof value.purpose === "string" &&
832
+ "issued_at_ms" in value &&
833
+ typeof value.issued_at_ms === "number" &&
834
+ "ttl_ms" in value &&
835
+ typeof value.ttl_ms === "number" &&
836
+ (!("binding" in value) || value.binding === undefined || isChoiceBinding(value.binding)) &&
837
+ "prefix" in value &&
838
+ typeof value.prefix === "string" &&
839
+ "payload" in value &&
840
+ isChoicePayload(value.payload) &&
841
+ "payload_digest" in value &&
842
+ typeof value.payload_digest === "string" &&
843
+ "replay_key" in value &&
844
+ typeof value.replay_key === "string"
845
+ );
846
+ }
847
+
848
+ function wordChoiceNotFoundError(): ProviderChoiceTokenError {
849
+ return new ProviderChoiceTokenError("invalid_payload", WORD_CHOICE_NOT_FOUND_MESSAGE);
377
850
  }
378
851
 
379
852
  type ServerProviderChoiceStorageOptions = Extract<
@@ -437,6 +910,11 @@ function stateTtl(
437
910
  return storage.ttl ?? `${ttlMs ?? 1}ms`;
438
911
  }
439
912
 
913
+ function remainingStateTtl(expiresAt: string): ProviderStateDurationString {
914
+ const remainingMs = Date.parse(expiresAt) - Date.now();
915
+ return `${Number.isFinite(remainingMs) ? Math.max(1, Math.floor(remainingMs)) : 1}ms`;
916
+ }
917
+
440
918
  function optionsStateKey(stateId: string): string {
441
919
  return stateId;
442
920
  }
@@ -449,6 +927,10 @@ function digestChoicePayload(serializedPayload: string): string {
449
927
  return createHash("sha256").update(serializedPayload).digest("base64url");
450
928
  }
451
929
 
930
+ function digestChoiceReplayKey(token: string): string {
931
+ return createHash("sha256").update(token).digest("hex");
932
+ }
933
+
452
934
  function isServerChoiceHandlePayload(
453
935
  value: ProviderChoiceTokenPayload,
454
936
  ): value is ServerChoiceHandlePayload {
@@ -621,6 +1103,35 @@ function createChoiceBinding(options: {
621
1103
  };
622
1104
  }
623
1105
 
1106
+ function hasRequestedChoiceBinding(options?: ProviderChoiceBindingOptions): boolean {
1107
+ return options?.connection === true || Boolean(options?.credentialKeys?.length);
1108
+ }
1109
+
1110
+ function assertWordChoiceBindingMatches(options: {
1111
+ readonly actual: ManagedChoiceEnvelope["binding"];
1112
+ readonly requested?: ProviderChoiceBindingOptions;
1113
+ readonly request?: ProviderRequestContext;
1114
+ readonly credential?: CredentialContext;
1115
+ readonly resolveKeys: () => ManagedChoiceKeys;
1116
+ }): void {
1117
+ const hasStoredBinding = Boolean(
1118
+ options.actual?.connection_hash || options.actual?.credential_hash,
1119
+ );
1120
+ const hasRequestedBinding = hasRequestedChoiceBinding(options.requested);
1121
+ if (!hasStoredBinding && !hasRequestedBinding) return;
1122
+ if (hasStoredBinding !== hasRequestedBinding) throw wordChoiceNotFoundError();
1123
+ assertChoiceBindingMatches({
1124
+ actual: options.actual,
1125
+ expected: createChoiceBinding({
1126
+ keys: options.resolveKeys(),
1127
+ options: options.requested,
1128
+ request: options.request,
1129
+ credential: options.credential,
1130
+ required: true,
1131
+ }),
1132
+ });
1133
+ }
1134
+
624
1135
  function hashRequiredConnection(options: {
625
1136
  readonly keys: ManagedChoiceKeys;
626
1137
  readonly request?: ProviderRequestContext;