@apifuse/provider-sdk 2.2.0-beta.3 → 2.2.0-beta.31

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 (342) hide show
  1. package/AUTHORING.md +493 -5
  2. package/CHANGELOG.md +131 -1
  3. package/README.md +52 -6
  4. package/SUBMISSION.md +1 -1
  5. package/bin/apifuse-check.ts +106 -62
  6. package/bin/apifuse-create.ts +1 -1
  7. package/bin/apifuse-dev.ts +63 -55
  8. package/bin/apifuse-pack-check.ts +22 -2
  9. package/bin/apifuse-pack-smoke.ts +78 -82
  10. package/bin/apifuse-pack-types.ts +583 -0
  11. package/bin/apifuse-perf.ts +59 -140
  12. package/bin/apifuse-record.ts +698 -113
  13. package/bin/apifuse-submit-check.ts +517 -44
  14. package/bin/apifuse-sync-assets.ts +117 -0
  15. package/bin/apifuse.ts +1 -1
  16. package/bin/submit-check-delimited-text.ts +50 -0
  17. package/bin/submit-check-xml.ts +1 -1
  18. package/dist/auth-turn/index.d.ts +4 -4
  19. package/dist/auth-turn/index.js +1 -1
  20. package/dist/auth.d.ts +16 -2
  21. package/dist/auth.js +76 -18
  22. package/dist/ceremonies/index.d.ts +9 -1
  23. package/dist/ceremonies/index.js +117 -29
  24. package/dist/cli/commands.d.ts +1 -1
  25. package/dist/cli/commands.js +8 -0
  26. package/dist/cli/create.d.ts +3 -0
  27. package/dist/cli/create.js +34 -35
  28. package/dist/cli/prompt-assets.d.ts +80 -0
  29. package/dist/cli/prompt-assets.js +743 -0
  30. package/dist/cli/templates/provider/AGENTS.md.tpl +17 -8
  31. package/dist/cli/templates/provider/README.md.tpl +4 -4
  32. package/dist/config/loader.d.ts +176 -17
  33. package/dist/config/loader.js +434 -161
  34. package/dist/contract-serialization.d.ts +2 -2
  35. package/dist/contract-serialization.js +7 -14
  36. package/dist/contract-types.d.ts +3 -2
  37. package/dist/contract.d.ts +3 -3
  38. package/dist/contract.js +6 -6
  39. package/dist/declaration-validation.d.ts +23 -0
  40. package/dist/declaration-validation.js +159 -0
  41. package/dist/define.d.ts +13 -1
  42. package/dist/define.js +391 -122
  43. package/dist/dev.d.ts +1 -1
  44. package/dist/dev.js +1 -1
  45. package/dist/error-resolution.d.ts +4 -0
  46. package/dist/error-resolution.js +123 -0
  47. package/dist/errors.d.ts +19 -1
  48. package/dist/errors.js +41 -3
  49. package/dist/fixture-sanitization.d.ts +26 -0
  50. package/dist/fixture-sanitization.js +216 -0
  51. package/dist/i18n/catalog.d.ts +2 -2
  52. package/dist/i18n/catalog.js +4 -10
  53. package/dist/i18n/index.d.ts +2 -2
  54. package/dist/i18n/index.js +2 -2
  55. package/dist/i18n/keys.d.ts +2 -2
  56. package/dist/index.d.ts +50 -42
  57. package/dist/index.js +41 -37
  58. package/dist/lint.d.ts +6 -1
  59. package/dist/lint.js +370 -18
  60. package/dist/native-address.d.ts +43 -0
  61. package/dist/native-address.js +281 -0
  62. package/dist/native-egress-policy.d.ts +31 -0
  63. package/dist/native-egress-policy.js +288 -0
  64. package/dist/observability.d.ts +5 -2
  65. package/dist/observability.js +48 -1
  66. package/dist/provider.d.ts +13 -11
  67. package/dist/provider.js +10 -9
  68. package/dist/public-schema-field-lint.d.ts +1 -1
  69. package/dist/recipes/gov-api.js +1 -1
  70. package/dist/runtime/auth-flow.d.ts +3 -1
  71. package/dist/runtime/auth-flow.js +8 -3
  72. package/dist/runtime/browser.d.ts +1 -1
  73. package/dist/runtime/browser.js +138 -40
  74. package/dist/runtime/cache.d.ts +2 -1
  75. package/dist/runtime/cache.js +173 -23
  76. package/dist/runtime/choice-wordlist.d.ts +9 -0
  77. package/dist/runtime/choice-wordlist.js +138 -0
  78. package/dist/runtime/choice.d.ts +14 -1
  79. package/dist/runtime/choice.js +566 -101
  80. package/dist/runtime/credential.d.ts +1 -1
  81. package/dist/runtime/credential.js +1 -1
  82. package/dist/runtime/env.d.ts +1 -1
  83. package/dist/runtime/executor.d.ts +1 -1
  84. package/dist/runtime/executor.js +25 -3
  85. package/dist/runtime/http.d.ts +3 -2
  86. package/dist/runtime/http.js +517 -55
  87. package/dist/runtime/insights.d.ts +1 -1
  88. package/dist/runtime/insights.js +6 -13
  89. package/dist/runtime/instrumentation.d.ts +2 -2
  90. package/dist/runtime/instrumentation.js +371 -23
  91. package/dist/runtime/keyring.js +1 -1
  92. package/dist/runtime/namespace.js +1 -1
  93. package/dist/runtime/native-network-errors.d.ts +33 -0
  94. package/dist/runtime/native-network-errors.js +69 -0
  95. package/dist/runtime/native-network.d.ts +96 -0
  96. package/dist/runtime/native-network.js +1232 -0
  97. package/dist/runtime/ocr.d.ts +29 -0
  98. package/dist/runtime/ocr.js +440 -0
  99. package/dist/runtime/otlp.d.ts +1 -1
  100. package/dist/runtime/perf.d.ts +1 -1
  101. package/dist/runtime/provider.d.ts +1 -1
  102. package/dist/runtime/provider.js +1 -2
  103. package/dist/runtime/proxy-errors.d.ts +1 -1
  104. package/dist/runtime/proxy-errors.js +9 -7
  105. package/dist/runtime/proxy-nodemaven.d.ts +56 -0
  106. package/dist/runtime/proxy-nodemaven.js +146 -0
  107. package/dist/runtime/proxy-retry-policy.d.ts +2 -2
  108. package/dist/runtime/proxy-retry-policy.js +2 -2
  109. package/dist/runtime/proxy-telemetry.d.ts +2 -1
  110. package/dist/runtime/proxy-telemetry.js +58 -52
  111. package/dist/runtime/redirects.d.ts +29 -0
  112. package/dist/runtime/redirects.js +36 -0
  113. package/dist/runtime/redis.d.ts +1 -1
  114. package/dist/runtime/redis.js +5 -5
  115. package/dist/runtime/request-options.d.ts +68 -1
  116. package/dist/runtime/request-options.js +548 -0
  117. package/dist/runtime/resolver-config.d.ts +6 -0
  118. package/dist/runtime/resolver-config.js +6 -0
  119. package/dist/runtime/resolver-public.d.ts +1 -0
  120. package/dist/runtime/resolver-public.js +1 -0
  121. package/dist/runtime/resolver-shared.d.ts +3 -0
  122. package/dist/runtime/resolver-shared.js +12 -0
  123. package/dist/runtime/resolver-vendors/bindings.d.ts +48 -0
  124. package/dist/runtime/resolver-vendors/bindings.js +40 -0
  125. package/dist/runtime/resolver-vendors/browser.d.ts +20 -0
  126. package/dist/runtime/resolver-vendors/browser.js +282 -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 +368 -0
  131. package/dist/runtime/resolver-vendors/types.d.ts +83 -0
  132. package/dist/runtime/resolver-vendors/types.js +69 -0
  133. package/dist/runtime/resolver.d.ts +59 -0
  134. package/dist/runtime/resolver.js +705 -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 +5 -2
  138. package/dist/runtime/state.js +280 -74
  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 +30 -5
  142. package/dist/runtime/stealth.js +523 -259
  143. package/dist/runtime/stt.d.ts +1 -1
  144. package/dist/runtime/stt.js +12 -27
  145. package/dist/runtime/timeout.d.ts +5 -0
  146. package/dist/runtime/timeout.js +12 -0
  147. package/dist/runtime/trace.d.ts +2 -2
  148. package/dist/runtime/trace.js +2 -4
  149. package/dist/runtime/waterfall.d.ts +1 -1
  150. package/dist/schema.d.ts +1 -1
  151. package/dist/schema.js +7 -15
  152. package/dist/serve.d.ts +1 -1
  153. package/dist/serve.js +1 -1
  154. package/dist/server/index.d.ts +7 -7
  155. package/dist/server/index.js +6 -6
  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-redaction.d.ts +1 -1
  159. package/dist/server/self-test-redaction.js +1 -1
  160. package/dist/server/self-test.d.ts +117 -3
  161. package/dist/server/self-test.js +787 -151
  162. package/dist/server/serve-implementation.d.ts +210 -0
  163. package/dist/server/serve-implementation.js +2078 -0
  164. package/dist/server/serve.d.ts +1 -70
  165. package/dist/server/serve.js +1 -1143
  166. package/dist/server/types.d.ts +34 -9
  167. package/dist/server/types.js +8 -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 +1 -1
  207. package/dist/stealth/profiles.js +21 -21
  208. package/dist/stream-evidence.d.ts +74 -0
  209. package/dist/stream-evidence.js +785 -0
  210. package/dist/stream.d.ts +1 -1
  211. package/dist/stream.js +7 -1
  212. package/dist/testing/index.d.ts +3 -2
  213. package/dist/testing/index.js +3 -2
  214. package/dist/testing/run.d.ts +32 -2
  215. package/dist/testing/run.js +488 -28
  216. package/dist/types.d.ts +566 -19
  217. package/dist/types.js +1 -0
  218. package/dist/user-input.d.ts +30 -0
  219. package/dist/user-input.js +66 -0
  220. package/package.json +42 -7
  221. package/src/auth-turn/index.ts +2 -2
  222. package/src/auth.ts +146 -86
  223. package/src/ceremonies/index.ts +167 -92
  224. package/src/cli/commands.ts +10 -0
  225. package/src/cli/create.ts +42 -35
  226. package/src/cli/prompt-assets.ts +865 -0
  227. package/src/cli/templates/provider/AGENTS.md.tpl +17 -8
  228. package/src/cli/templates/provider/README.md.tpl +4 -4
  229. package/src/config/loader.ts +667 -289
  230. package/src/contract-serialization.ts +10 -18
  231. package/src/contract-types.ts +3 -2
  232. package/src/contract.ts +14 -28
  233. package/src/declaration-validation.ts +202 -0
  234. package/src/define.ts +631 -495
  235. package/src/dev.ts +4 -9
  236. package/src/error-resolution.ts +128 -0
  237. package/src/errors.ts +56 -11
  238. package/src/fixture-sanitization.ts +247 -0
  239. package/src/i18n/catalog.ts +10 -32
  240. package/src/i18n/index.ts +2 -2
  241. package/src/i18n/keys.ts +5 -11
  242. package/src/index.ts +158 -44
  243. package/src/lint.ts +488 -154
  244. package/src/native-address.ts +340 -0
  245. package/src/native-egress-policy.ts +358 -0
  246. package/src/observability.ts +51 -1
  247. package/src/provider.ts +66 -11
  248. package/src/public-schema-field-lint.ts +7 -33
  249. package/src/recipes/gov-api.ts +2 -5
  250. package/src/runtime/auth-flow.ts +13 -7
  251. package/src/runtime/browser.ts +252 -207
  252. package/src/runtime/cache.ts +209 -81
  253. package/src/runtime/choice-wordlist.ts +145 -0
  254. package/src/runtime/choice.ts +758 -197
  255. package/src/runtime/credential.ts +2 -2
  256. package/src/runtime/env.ts +1 -1
  257. package/src/runtime/executor.ts +37 -19
  258. package/src/runtime/http.ts +645 -65
  259. package/src/runtime/insights.ts +15 -53
  260. package/src/runtime/instrumentation.ts +530 -67
  261. package/src/runtime/keyring.ts +7 -19
  262. package/src/runtime/namespace.ts +2 -7
  263. package/src/runtime/native-network-errors.ts +99 -0
  264. package/src/runtime/native-network.ts +1605 -0
  265. package/src/runtime/ocr.ts +523 -0
  266. package/src/runtime/otlp.ts +12 -23
  267. package/src/runtime/perf.ts +1 -1
  268. package/src/runtime/provider.ts +4 -9
  269. package/src/runtime/proxy-errors.ts +29 -42
  270. package/src/runtime/proxy-nodemaven.ts +221 -0
  271. package/src/runtime/proxy-retry-policy.ts +3 -3
  272. package/src/runtime/proxy-telemetry.ts +84 -77
  273. package/src/runtime/redirects.ts +66 -0
  274. package/src/runtime/redis.ts +10 -13
  275. package/src/runtime/request-options.ts +679 -9
  276. package/src/runtime/resolver-config.ts +6 -0
  277. package/src/runtime/resolver-public.ts +18 -0
  278. package/src/runtime/resolver-shared.ts +17 -0
  279. package/src/runtime/resolver-vendors/bindings.ts +56 -0
  280. package/src/runtime/resolver-vendors/browser.ts +408 -0
  281. package/src/runtime/resolver-vendors/hosts.ts +38 -0
  282. package/src/runtime/resolver-vendors/twocaptcha.ts +500 -0
  283. package/src/runtime/resolver-vendors/types.ts +173 -0
  284. package/src/runtime/resolver.ts +1060 -0
  285. package/src/runtime/secrets.ts +64 -0
  286. package/src/runtime/state.ts +399 -161
  287. package/src/runtime/stealth-cookies.ts +132 -0
  288. package/src/runtime/stealth.ts +681 -295
  289. package/src/runtime/stt.ts +39 -113
  290. package/src/runtime/timeout.ts +18 -0
  291. package/src/runtime/trace.ts +14 -44
  292. package/src/runtime/waterfall.ts +5 -18
  293. package/src/schema.ts +23 -84
  294. package/src/serve.ts +6 -1
  295. package/src/server/index.ts +30 -7
  296. package/src/server/self-test-input-tokens.ts +29 -14
  297. package/src/server/self-test-redaction.ts +2 -2
  298. package/src/server/self-test.ts +1030 -180
  299. package/src/server/serve-implementation.ts +3062 -0
  300. package/src/server/serve.ts +1 -1781
  301. package/src/server/types.ts +12 -13
  302. package/src/stateful/README.md +146 -0
  303. package/src/stateful/errors.ts +35 -0
  304. package/src/stateful/http-provider-event-emitter.ts +314 -0
  305. package/src/stateful/http-session-owner-registry.ts +306 -0
  306. package/src/stateful/index.ts +18 -0
  307. package/src/stateful/provider-event-delivery-failures.ts +80 -0
  308. package/src/stateful/provider-event-pipeline-metrics.ts +95 -0
  309. package/src/stateful/provider-event-pipeline.ts +61 -0
  310. package/src/stateful/provider-events.ts +462 -0
  311. package/src/stateful/session-key.ts +111 -0
  312. package/src/stateful/stateful-provider-adapter-context.ts +59 -0
  313. package/src/stateful/stateful-provider-adapter-metrics.ts +48 -0
  314. package/src/stateful/stateful-provider-adapter.ts +562 -0
  315. package/src/stateful/stateful-provider-observability.ts +261 -0
  316. package/src/stateful/stateful-provider-owner-forwarder.ts +287 -0
  317. package/src/stateful/stateful-provider-runtime-context.ts +92 -0
  318. package/src/stateful/stateful-provider-runtime-executor.ts +96 -0
  319. package/src/stateful/stateful-provider-session-routing.ts +546 -0
  320. package/src/stateful/stateful-provider-session-runtime.ts +403 -0
  321. package/src/stateful-signing.ts +46 -0
  322. package/src/stealth/profiles.ts +27 -33
  323. package/src/stream-evidence.ts +988 -0
  324. package/src/stream.ts +16 -20
  325. package/src/testing/index.ts +11 -2
  326. package/src/testing/run.ts +668 -74
  327. package/src/types.ts +665 -35
  328. package/src/user-input.ts +118 -0
  329. package/dist/cli/templates/provider/CLAUDE.md.tpl +0 -1
  330. package/src/cli/templates/provider/CLAUDE.md.tpl +0 -1
  331. /package/dist/cli/templates/provider/{skills → .agents/skills}/fixtures-and-recording/SKILL.md.tpl +0 -0
  332. /package/dist/cli/templates/provider/{skills → .agents/skills}/health-checks-and-fail-closed/SKILL.md.tpl +0 -0
  333. /package/dist/cli/templates/provider/{skills → .agents/skills}/normalization-standards/SKILL.md.tpl +0 -0
  334. /package/dist/cli/templates/provider/{skills → .agents/skills}/pagination-and-counts/SKILL.md.tpl +0 -0
  335. /package/dist/cli/templates/provider/{skills → .agents/skills}/upstream-contract-verification/SKILL.md.tpl +0 -0
  336. /package/dist/cli/templates/provider/{skills → .agents/skills}/upstream-notes/README.md.tpl +0 -0
  337. /package/src/cli/templates/provider/{skills → .agents/skills}/fixtures-and-recording/SKILL.md.tpl +0 -0
  338. /package/src/cli/templates/provider/{skills → .agents/skills}/health-checks-and-fail-closed/SKILL.md.tpl +0 -0
  339. /package/src/cli/templates/provider/{skills → .agents/skills}/normalization-standards/SKILL.md.tpl +0 -0
  340. /package/src/cli/templates/provider/{skills → .agents/skills}/pagination-and-counts/SKILL.md.tpl +0 -0
  341. /package/src/cli/templates/provider/{skills → .agents/skills}/upstream-contract-verification/SKILL.md.tpl +0 -0
  342. /package/src/cli/templates/provider/{skills → .agents/skills}/upstream-notes/README.md.tpl +0 -0
@@ -1,14 +1,74 @@
1
- import { createCipheriv, createDecipheriv, createHash, createHmac, randomBytes, timingSafeEqual, } from "node:crypto";
2
- import { assertFreshProviderChoiceIssuedAt, ProviderChoiceTokenError, } from "../choice-token";
3
- import { ProviderError } from "../errors";
1
+ import { createCipheriv, createDecipheriv, createHash, createHmac, randomBytes, randomInt, timingSafeEqual, } from "node:crypto";
2
+ import { assertFreshProviderChoiceIssuedAt, ProviderChoiceTokenError, } from "../choice-token.js";
3
+ import { isProviderError, ProviderError } from "../errors.js";
4
+ import { CHOICE_WORDLIST_SIZE, choiceWordAt, HIGH_CHOICE_WORD_COUNT, isChoiceWord, STANDARD_CHOICE_WORD_COUNT, } from "./choice-wordlist.js";
4
5
  export const PROVIDER_RUNTIME_CHOICE_TOKEN_MASTER_SECRET_ENV = "APIFUSE__PROVIDER_RUNTIME__CHOICE_TOKEN_MASTER_SECRET";
6
+ export const PROVIDER_RUNTIME_CHOICE_WORD_ISSUANCE_ENV = "APIFUSE__PROVIDER_RUNTIME__CHOICE_WORD_ISSUANCE";
5
7
  const PRIMARY_CHOICE_TOKEN_KID = "v1";
6
8
  const MANAGED_CHOICE_TOKEN_VERSION = 1;
9
+ const SERVER_STORED_CHOICE_RECORD_VERSION = 1;
10
+ const SERVER_STORED_CHOICE_ISSUE_ATTEMPTS = 5;
11
+ const WORD_CHOICE_NOT_FOUND_MESSAGE = "Provider choice token was not found.";
7
12
  export function createProviderChoiceContext(options) {
8
13
  const kid = options.kid ?? PRIMARY_CHOICE_TOKEN_KID;
9
14
  const resolveMasterSecret = () => resolveChoiceMasterSecret(options);
10
15
  function issue(issueOptions) {
11
16
  const issuedAtMs = issueOptions.nowMs ?? Date.now();
17
+ const resolvedStorage = resolveIssueStorage(issueOptions.storage, issueOptions.payload);
18
+ if (resolvedStorage.mode === "server") {
19
+ const issuance = resolveChoiceWordIssuance(options.env);
20
+ const keys = hasRequestedChoiceBinding(issueOptions.bind)
21
+ ? deriveManagedChoiceKeys({
22
+ masterSecret: resolveMasterSecret(),
23
+ providerId: options.providerId,
24
+ purpose: issueOptions.purpose,
25
+ kid,
26
+ })
27
+ : undefined;
28
+ const binding = hasRequestedChoiceBinding(issueOptions.bind)
29
+ ? createChoiceBinding({
30
+ keys: keys,
31
+ options: issueOptions.bind,
32
+ request: options.request,
33
+ credential: options.credential,
34
+ required: true,
35
+ })
36
+ : undefined;
37
+ const baseEnvelope = {
38
+ v: MANAGED_CHOICE_TOKEN_VERSION,
39
+ provider_id: options.providerId,
40
+ purpose: issueOptions.purpose,
41
+ issued_at_ms: issuedAtMs,
42
+ ttl_ms: issueOptions.ttlMs,
43
+ binding,
44
+ };
45
+ if (issuance === "legacy") {
46
+ const legacyKeys = keys ??
47
+ deriveManagedChoiceKeys({
48
+ masterSecret: resolveMasterSecret(),
49
+ providerId: options.providerId,
50
+ purpose: issueOptions.purpose,
51
+ kid,
52
+ });
53
+ return issueLegacyServerStoredChoice({
54
+ baseEnvelope,
55
+ issueOptions,
56
+ storage: resolvedStorage.storage,
57
+ contextState: options.state,
58
+ kid,
59
+ keys: legacyKeys,
60
+ issuedAtMs,
61
+ });
62
+ }
63
+ return issueWordServerStoredChoice({
64
+ baseEnvelope: {
65
+ ...baseEnvelope,
66
+ },
67
+ issueOptions,
68
+ storage: resolvedStorage.storage,
69
+ contextState: options.state,
70
+ });
71
+ }
12
72
  const keys = deriveManagedChoiceKeys({
13
73
  masterSecret: resolveMasterSecret(),
14
74
  providerId: options.providerId,
@@ -29,18 +89,6 @@ export function createProviderChoiceContext(options) {
29
89
  required: true,
30
90
  }),
31
91
  };
32
- const resolvedStorage = resolveIssueStorage(issueOptions.storage, issueOptions.payload);
33
- if (resolvedStorage.mode === "server") {
34
- return issueServerStoredChoice({
35
- baseEnvelope,
36
- issueOptions,
37
- storage: resolvedStorage.storage,
38
- contextState: options.state,
39
- kid,
40
- keys,
41
- issuedAtMs,
42
- });
43
- }
44
92
  const envelope = {
45
93
  ...baseEnvelope,
46
94
  payload: issueOptions.payload,
@@ -53,77 +101,228 @@ export function createProviderChoiceContext(options) {
53
101
  });
54
102
  }
55
103
  function parse(parseOptions) {
56
- const [actualPrefix, tokenKid, encodedIv, encryptedPayload, authTag, signature,] = parseManagedChoiceTokenParts(parseOptions.token);
57
- if (actualPrefix !== parseOptions.prefix ||
58
- tokenKid !== kid ||
59
- !encodedIv ||
60
- !encryptedPayload ||
61
- !authTag ||
62
- !signature) {
63
- throw new ProviderChoiceTokenError("invalid_shape", "Provider choice token shape is invalid.");
64
- }
65
- const keys = deriveManagedChoiceKeys({
66
- masterSecret: resolveMasterSecret(),
67
- providerId: options.providerId,
68
- purpose: parseOptions.purpose,
69
- kid: tokenKid,
104
+ const consumeMode = parseOptions.consume ?? "never";
105
+ const wordStateKey = parseWordChoiceStateKey({
106
+ token: parseOptions.token,
107
+ prefix: parseOptions.prefix,
70
108
  });
71
- const signedBody = [
72
- parseOptions.prefix,
73
- tokenKid,
74
- encodedIv,
75
- encryptedPayload,
76
- authTag,
77
- ].join(".");
78
- assertManagedChoiceSignature({
79
- signedBody,
80
- signature,
81
- signingKey: keys.signing,
82
- });
83
- const envelope = decryptManagedChoiceToken({
84
- encodedIv,
85
- encryptedPayload,
86
- authTag,
87
- encryptionKey: keys.encryption,
88
- });
89
- assertManagedChoiceEnvelope(envelope, {
90
- providerId: options.providerId,
91
- purpose: parseOptions.purpose,
92
- ttlMs: parseOptions.ttlMs,
93
- nowMs: parseOptions.nowMs,
94
- futureToleranceMs: parseOptions.futureToleranceMs,
95
- });
96
- assertChoiceBindingMatches({
97
- actual: envelope.binding,
98
- expected: createChoiceBinding({
99
- keys,
100
- options: parseOptions.bind,
109
+ if (wordStateKey) {
110
+ const parsed = parseWordServerStoredChoice({
111
+ stateKey: wordStateKey,
112
+ parseOptions,
113
+ contextState: options.state,
114
+ providerId: options.providerId,
101
115
  request: options.request,
102
116
  credential: options.credential,
103
- required: true,
104
- }),
105
- });
106
- if (isServerChoiceHandlePayload(envelope.payload)) {
107
- return parseServerStoredChoice({
108
- handle: envelope.payload,
109
- storage: parseOptions.storage,
110
- contextState: options.state,
117
+ resolveBindingKeys: () => deriveManagedChoiceKeys({
118
+ masterSecret: resolveMasterSecret(),
119
+ providerId: options.providerId,
120
+ purpose: parseOptions.purpose,
121
+ kid,
122
+ }),
123
+ onConsume: (result) => emitChoiceTelemetry(options.onTelemetry, {
124
+ providerId: options.providerId,
125
+ purpose: parseOptions.purpose,
126
+ operation: "consume",
127
+ format: "word",
128
+ outcome: "success",
129
+ consumeMode,
130
+ consumed: result.status === "consumed",
131
+ replay: result.status === "already-consumed",
132
+ }),
133
+ });
134
+ return observeChoiceParse(parsed, {
135
+ onTelemetry: options.onTelemetry,
136
+ providerId: options.providerId,
137
+ purpose: parseOptions.purpose,
138
+ format: "word",
139
+ consumeMode,
140
+ });
141
+ }
142
+ // Legacy encrypted-envelope compatibility fallback. Removal is gated on
143
+ // the last legacy mint plus the maximum issued TTL; see ADR 0006.
144
+ // A structurally valid word token returns above, so lookup, expiry,
145
+ // consumption, and binding failures can never enter this branch.
146
+ try {
147
+ const [actualPrefix, tokenKid, encodedIv, encryptedPayload, authTag, signature] = parseManagedChoiceTokenParts(parseOptions.token);
148
+ if (actualPrefix !== parseOptions.prefix ||
149
+ tokenKid !== kid ||
150
+ !encodedIv ||
151
+ !encryptedPayload ||
152
+ !authTag ||
153
+ !signature) {
154
+ throw new ProviderChoiceTokenError("invalid_shape", "Provider choice token shape is invalid.");
155
+ }
156
+ const keys = deriveManagedChoiceKeys({
157
+ masterSecret: resolveMasterSecret(),
158
+ providerId: options.providerId,
159
+ purpose: parseOptions.purpose,
160
+ kid: tokenKid,
161
+ });
162
+ const signedBody = [parseOptions.prefix, tokenKid, encodedIv, encryptedPayload, authTag].join(".");
163
+ assertManagedChoiceSignature({
164
+ signedBody,
165
+ signature,
166
+ signingKey: keys.signing,
167
+ });
168
+ const envelope = decryptManagedChoiceToken({
169
+ encodedIv,
170
+ encryptedPayload,
171
+ authTag,
172
+ encryptionKey: keys.encryption,
173
+ });
174
+ assertManagedChoiceEnvelope(envelope, {
175
+ providerId: options.providerId,
176
+ purpose: parseOptions.purpose,
177
+ ttlMs: parseOptions.ttlMs,
178
+ nowMs: parseOptions.nowMs,
179
+ futureToleranceMs: parseOptions.futureToleranceMs,
180
+ });
181
+ assertChoiceBindingMatches({
182
+ actual: envelope.binding,
183
+ expected: createChoiceBinding({
184
+ keys,
185
+ options: parseOptions.bind,
186
+ request: options.request,
187
+ credential: options.credential,
188
+ required: true,
189
+ }),
190
+ });
191
+ const payload = isServerChoiceHandlePayload(envelope.payload)
192
+ ? parseLegacyServerStoredChoice({
193
+ handle: envelope.payload,
194
+ storage: parseOptions.storage,
195
+ contextState: options.state,
196
+ })
197
+ : envelope.payload;
198
+ const parsed = consumeMode === "explicit"
199
+ ? Promise.resolve(payload).then((resolvedPayload) => createLegacyExplicitParseResult({
200
+ payload: resolvedPayload,
201
+ replayKey: digestChoiceReplayKey(parseOptions.token),
202
+ onConsume: () => emitChoiceTelemetry(options.onTelemetry, {
203
+ providerId: options.providerId,
204
+ purpose: parseOptions.purpose,
205
+ operation: "consume",
206
+ format: "legacy",
207
+ outcome: "unsupported",
208
+ consumeMode,
209
+ consumed: false,
210
+ replay: false,
211
+ }),
212
+ }))
213
+ : payload;
214
+ return observeChoiceParse(parsed, {
215
+ onTelemetry: options.onTelemetry,
216
+ providerId: options.providerId,
217
+ purpose: parseOptions.purpose,
218
+ format: "legacy",
219
+ consumeMode,
220
+ });
221
+ }
222
+ catch (error) {
223
+ emitChoiceParseFailure(options.onTelemetry, error, {
224
+ providerId: options.providerId,
225
+ purpose: parseOptions.purpose,
226
+ format: "legacy",
227
+ consumeMode,
111
228
  });
229
+ throw error;
112
230
  }
113
- return envelope.payload;
114
231
  }
115
232
  return { issue, parse };
116
233
  }
117
234
  export function createTestProviderChoiceContext(options) {
118
235
  return createProviderChoiceContext({
119
236
  ...options,
120
- masterSecret: options.masterSecret ??
121
- "apifuse-test-provider-runtime-choice-token-master-secret",
237
+ masterSecret: options.masterSecret ?? "apifuse-test-provider-runtime-choice-token-master-secret",
238
+ });
239
+ }
240
+ function observeChoiceParse(result, base) {
241
+ if (result instanceof Promise) {
242
+ return result.then((value) => {
243
+ emitChoiceParseSuccess(base, value);
244
+ return value;
245
+ }, (error) => {
246
+ emitChoiceParseFailure(base.onTelemetry, error, base);
247
+ throw error;
248
+ });
249
+ }
250
+ emitChoiceParseSuccess(base, result);
251
+ return result;
252
+ }
253
+ function emitChoiceParseSuccess(base, result) {
254
+ const replay = isConsumedChoiceReplay(result);
255
+ emitChoiceTelemetry(base.onTelemetry, {
256
+ providerId: base.providerId,
257
+ purpose: base.purpose,
258
+ operation: "parse",
259
+ format: base.format,
260
+ outcome: "success",
261
+ consumeMode: base.consumeMode,
262
+ consumed: replay || (base.format === "word" && base.consumeMode === "on-parse"),
263
+ replay,
264
+ });
265
+ }
266
+ function isConsumedChoiceReplay(value) {
267
+ return (value !== null &&
268
+ typeof value === "object" &&
269
+ "status" in value &&
270
+ value.status === "consumed" &&
271
+ "replayKey" in value &&
272
+ typeof value.replayKey === "string");
273
+ }
274
+ function emitChoiceParseFailure(onTelemetry, error, base) {
275
+ const outcome = error instanceof ProviderChoiceTokenError
276
+ ? base.format === "word" && error.message === WORD_CHOICE_NOT_FOUND_MESSAGE
277
+ ? "not-found"
278
+ : "invalid"
279
+ : "error";
280
+ emitChoiceTelemetry(onTelemetry, {
281
+ providerId: base.providerId,
282
+ purpose: base.purpose,
283
+ operation: "parse",
284
+ format: base.format,
285
+ outcome,
286
+ consumeMode: base.consumeMode,
287
+ consumed: false,
288
+ replay: false,
289
+ });
290
+ }
291
+ function emitChoiceTelemetry(onTelemetry, event) {
292
+ try {
293
+ onTelemetry?.(event);
294
+ }
295
+ catch {
296
+ // Observability must never change provider token semantics.
297
+ }
298
+ }
299
+ function createLegacyExplicitParseResult(options) {
300
+ return {
301
+ status: "active",
302
+ payload: options.payload,
303
+ replayKey: options.replayKey,
304
+ consume: async () => {
305
+ options.onConsume();
306
+ return { status: "unsupported" };
307
+ },
308
+ };
309
+ }
310
+ function resolveChoiceWordIssuance(env) {
311
+ const configured = env?.get(PROVIDER_RUNTIME_CHOICE_WORD_ISSUANCE_ENV);
312
+ const value = configured?.trim() ?? "";
313
+ if (value === "" || value === "legacy")
314
+ return "legacy";
315
+ if (value === "word")
316
+ return "word";
317
+ throw new ProviderError(`Unsupported provider choice word issuance mode "${value}". Expected "legacy" or "word".`, {
318
+ code: "CHOICE_WORD_ISSUANCE_INVALID",
319
+ category: "input_validation",
320
+ retryable: false,
321
+ details: { env: PROVIDER_RUNTIME_CHOICE_WORD_ISSUANCE_ENV },
122
322
  });
123
323
  }
124
324
  function resolveChoiceMasterSecret(options) {
125
- const configured = options.masterSecret ??
126
- options.env?.get(PROVIDER_RUNTIME_CHOICE_TOKEN_MASTER_SECRET_ENV);
325
+ const configured = options.masterSecret ?? options.env?.get(PROVIDER_RUNTIME_CHOICE_TOKEN_MASTER_SECRET_ENV);
127
326
  const trimmed = configured?.trim();
128
327
  if (trimmed)
129
328
  return trimmed;
@@ -165,19 +364,68 @@ function encryptManagedChoiceToken(options) {
165
364
  ]).toString("base64url");
166
365
  const authTag = cipher.getAuthTag().toString("base64url");
167
366
  const encodedIv = iv.toString("base64url");
168
- const signedBody = [
169
- options.prefix,
170
- options.kid,
171
- encodedIv,
172
- encryptedPayload,
173
- authTag,
174
- ].join(".");
367
+ const signedBody = [options.prefix, options.kid, encodedIv, encryptedPayload, authTag].join(".");
175
368
  const signature = createHmac("sha256", options.keys.signing)
176
369
  .update(signedBody)
177
370
  .digest("base64url");
178
371
  return `${signedBody}.${signature}`;
179
372
  }
180
- async function issueServerStoredChoice(options) {
373
+ async function issueWordServerStoredChoice(options) {
374
+ const serializedPayload = serializeChoicePayload(options.issueOptions.payload);
375
+ const payloadDigest = digestChoicePayload(serializedPayload);
376
+ const namespace = resolveChoiceStateNamespace({
377
+ storage: options.storage,
378
+ contextState: options.contextState,
379
+ ttlMs: options.issueOptions.ttlMs,
380
+ });
381
+ const wordCount = options.issueOptions.strength === "high" ? HIGH_CHOICE_WORD_COUNT : STANDARD_CHOICE_WORD_COUNT;
382
+ for (let attempt = 0; attempt < SERVER_STORED_CHOICE_ISSUE_ATTEMPTS; attempt += 1) {
383
+ const stateKey = generateChoiceWordSequence(wordCount);
384
+ const token = `${options.issueOptions.prefix}${stateKey}`;
385
+ const record = {
386
+ v: SERVER_STORED_CHOICE_RECORD_VERSION,
387
+ storage: "server",
388
+ status: "active",
389
+ provider_id: options.baseEnvelope.provider_id,
390
+ purpose: options.baseEnvelope.purpose,
391
+ issued_at_ms: options.baseEnvelope.issued_at_ms,
392
+ ttl_ms: options.baseEnvelope.ttl_ms,
393
+ binding: options.baseEnvelope.binding,
394
+ prefix: options.issueOptions.prefix,
395
+ payload: options.issueOptions.payload,
396
+ payload_digest: payloadDigest,
397
+ replay_key: digestChoiceReplayKey(token),
398
+ };
399
+ const valueBytes = Buffer.byteLength(JSON.stringify(record), "utf8");
400
+ if (valueBytes > options.storage.maxValueBytes) {
401
+ throw new ProviderError("Provider choice payload exceeds state storage policy.", {
402
+ code: "CHOICE_STATE_PAYLOAD_TOO_LARGE",
403
+ category: "input_validation",
404
+ retryable: false,
405
+ details: {
406
+ maxValueBytes: options.storage.maxValueBytes,
407
+ valueBytes,
408
+ },
409
+ });
410
+ }
411
+ const result = await namespace.compareAndSet(optionsStateKey(stateKey), 0, record, {
412
+ ttl: stateTtl(options.storage, options.issueOptions.ttlMs),
413
+ });
414
+ if (result.ok)
415
+ return token;
416
+ }
417
+ throw new ProviderError("Provider choice state storage is not available.", {
418
+ code: "CHOICE_STATE_UNAVAILABLE",
419
+ category: "internal_error",
420
+ retryable: false,
421
+ });
422
+ }
423
+ /**
424
+ * Beta.28-compatible server handle issuance. The state stores the payload and
425
+ * the client receives the existing six-part encrypted envelope whose payload
426
+ * is a server-state handle.
427
+ */
428
+ async function issueLegacyServerStoredChoice(options) {
181
429
  const serializedPayload = serializeChoicePayload(options.issueOptions.payload);
182
430
  const payloadBytes = Buffer.byteLength(serializedPayload, "utf8");
183
431
  if (payloadBytes > options.storage.maxValueBytes) {
@@ -185,14 +433,10 @@ async function issueServerStoredChoice(options) {
185
433
  code: "CHOICE_STATE_PAYLOAD_TOO_LARGE",
186
434
  category: "input_validation",
187
435
  retryable: false,
188
- details: {
189
- maxValueBytes: options.storage.maxValueBytes,
190
- payloadBytes,
191
- },
436
+ details: { maxValueBytes: options.storage.maxValueBytes, payloadBytes },
192
437
  });
193
438
  }
194
439
  const stateId = `choice_${randomBytes(16).toString("base64url")}`;
195
- const digest = digestChoicePayload(serializedPayload);
196
440
  const namespace = resolveChoiceStateNamespace({
197
441
  storage: options.storage,
198
442
  contextState: options.contextState,
@@ -206,7 +450,7 @@ async function issueServerStoredChoice(options) {
206
450
  payload: {
207
451
  storage: "server",
208
452
  state_id: stateId,
209
- payload_digest: digest,
453
+ payload_digest: digestChoicePayload(serializedPayload),
210
454
  created_at_ms: options.issuedAtMs,
211
455
  },
212
456
  };
@@ -217,13 +461,149 @@ async function issueServerStoredChoice(options) {
217
461
  keys: options.keys,
218
462
  });
219
463
  }
220
- async function parseServerStoredChoice(options) {
464
+ async function parseWordServerStoredChoice(options) {
465
+ const storage = resolveParseStorage(options.parseOptions.storage);
466
+ const namespace = resolveChoiceStateNamespace({
467
+ storage,
468
+ contextState: options.contextState,
469
+ ttlMs: options.parseOptions.ttlMs,
470
+ });
471
+ let stored;
472
+ try {
473
+ stored = await namespace.get(optionsStateKey(options.stateKey));
474
+ }
475
+ catch (error) {
476
+ if (isProviderError(error))
477
+ throw error;
478
+ throw wordChoiceNotFoundError();
479
+ }
480
+ if (!stored || !isServerStoredChoiceRecord(stored.value)) {
481
+ throw wordChoiceNotFoundError();
482
+ }
483
+ const record = stored.value;
484
+ const expectedReplayKey = digestChoiceReplayKey(`${options.parseOptions.prefix}${options.stateKey}`);
485
+ try {
486
+ if (record.provider_id !== options.providerId ||
487
+ record.purpose !== options.parseOptions.purpose ||
488
+ record.prefix !== options.parseOptions.prefix) {
489
+ throw wordChoiceNotFoundError();
490
+ }
491
+ assertFreshProviderChoiceIssuedAt(record.issued_at_ms, {
492
+ ttlMs: options.parseOptions.ttlMs != null
493
+ ? Math.min(options.parseOptions.ttlMs, record.ttl_ms)
494
+ : record.ttl_ms,
495
+ nowMs: options.parseOptions.nowMs,
496
+ futureToleranceMs: options.parseOptions.futureToleranceMs,
497
+ });
498
+ assertPayloadDigestMatches({
499
+ actual: digestChoicePayload(serializeChoicePayload(record.payload)),
500
+ expected: record.payload_digest,
501
+ });
502
+ assertPayloadDigestMatches({ actual: expectedReplayKey, expected: record.replay_key });
503
+ assertWordChoiceBindingMatches({
504
+ actual: record.binding,
505
+ requested: options.parseOptions.bind,
506
+ request: options.request,
507
+ credential: options.credential,
508
+ resolveKeys: options.resolveBindingKeys,
509
+ });
510
+ }
511
+ catch (error) {
512
+ if (error instanceof ProviderChoiceTokenError ||
513
+ (isProviderError(error) && error.code === "CHOICE_CONTEXT_REQUIRED")) {
514
+ throw wordChoiceNotFoundError();
515
+ }
516
+ throw error;
517
+ }
518
+ const consumeMode = options.parseOptions.consume ?? "never";
519
+ if (record.status === "consumed") {
520
+ if (consumeMode === "explicit") {
521
+ return { status: "consumed", replayKey: record.replay_key };
522
+ }
523
+ throw wordChoiceNotFoundError();
524
+ }
525
+ if (consumeMode === "never")
526
+ return record.payload;
527
+ if (consumeMode === "explicit") {
528
+ return {
529
+ status: "active",
530
+ payload: record.payload,
531
+ replayKey: record.replay_key,
532
+ consume: async () => {
533
+ const result = await consumeWordServerStoredChoice({
534
+ stateKey: options.stateKey,
535
+ stored,
536
+ record,
537
+ storage,
538
+ contextState: options.contextState,
539
+ });
540
+ options.onConsume(result);
541
+ return result;
542
+ },
543
+ };
544
+ }
545
+ const consumed = await consumeWordServerStoredChoice({
546
+ stateKey: options.stateKey,
547
+ stored,
548
+ record,
549
+ storage,
550
+ contextState: options.contextState,
551
+ });
552
+ if (consumed.status !== "consumed")
553
+ throw wordChoiceNotFoundError();
554
+ return record.payload;
555
+ }
556
+ async function consumeWordServerStoredChoice(options) {
557
+ const namespace = resolveChoiceStateNamespace({
558
+ storage: options.storage,
559
+ contextState: options.contextState,
560
+ ttlMs: options.record.ttl_ms,
561
+ });
562
+ try {
563
+ const consumed = await namespace.compareAndSet(optionsStateKey(options.stateKey), options.stored.version, { ...options.record, status: "consumed" }, { ttl: remainingStateTtl(options.stored.expiresAt) });
564
+ if (consumed.ok)
565
+ return { status: "consumed" };
566
+ if (consumed.current &&
567
+ isServerStoredChoiceRecord(consumed.current.value) &&
568
+ consumed.current.value.status === "consumed" &&
569
+ consumed.current.value.replay_key === options.record.replay_key) {
570
+ return { status: "already-consumed" };
571
+ }
572
+ throw wordChoiceNotFoundError();
573
+ }
574
+ catch (error) {
575
+ if (isProviderError(error))
576
+ throw error;
577
+ if (error instanceof ProviderChoiceTokenError)
578
+ throw error;
579
+ throw wordChoiceNotFoundError();
580
+ }
581
+ }
582
+ async function parseLegacyServerStoredChoice(options) {
221
583
  const storage = resolveParseStorage(options.storage);
222
584
  const namespace = resolveChoiceStateNamespace({
223
585
  storage,
224
586
  contextState: options.contextState,
225
587
  });
226
- const record = await namespace.get(optionsStateKey(options.handle.state_id));
588
+ // Reading a server-stored choice back deserializes a persisted value. A
589
+ // corrupt/undecodable value would otherwise surface as a raw JSON.parse
590
+ // SyntaxError (or another unexpected throwable) that escapes the choice error
591
+ // taxonomy, gets masked as internal_error 500, and is treated as retryable by
592
+ // the hub -> reservation restart loop (2026-07-22 catchtable RCA, candidate A).
593
+ // Convert any non-branded throwable into a branded invalid_payload so it maps
594
+ // to a clean, non-retryable 400. Branded ProviderChoiceTokenError and genuine
595
+ // ProviderError (e.g. Redis-unavailable / state-unavailable) pass through so
596
+ // their category/retryable semantics are preserved.
597
+ let record;
598
+ try {
599
+ record = await namespace.get(optionsStateKey(options.handle.state_id));
600
+ }
601
+ catch (error) {
602
+ if (error instanceof ProviderChoiceTokenError || isProviderError(error)) {
603
+ throw error;
604
+ }
605
+ throw new ProviderChoiceTokenError("invalid_payload", "Provider choice token state payload could not be decoded.");
606
+ }
227
607
  if (!record) {
228
608
  throw new ProviderChoiceTokenError("invalid_payload", "Provider choice token state payload is missing.");
229
609
  }
@@ -234,6 +614,71 @@ async function parseServerStoredChoice(options) {
234
614
  });
235
615
  return record.value;
236
616
  }
617
+ function generateChoiceWordSequence(wordCount) {
618
+ return Array.from({ length: wordCount }, () => choiceWordAt(randomInt(CHOICE_WORDLIST_SIZE))).join("-");
619
+ }
620
+ function parseWordChoiceStateKey(options) {
621
+ if (!options.token.startsWith(options.prefix))
622
+ return null;
623
+ const body = options.token.slice(options.prefix.length);
624
+ // The official list contains one hyphenated entry (`yo-yo`), so structural
625
+ // recognition uses dictionary-aware segmentation instead of assuming every
626
+ // hyphen is a word boundary.
627
+ if (!/^[a-z]+(?:-[a-z]+){3,9}$/.test(body))
628
+ return null;
629
+ const segments = body.split("-");
630
+ if (!canSegmentChoiceWords(segments, 0, STANDARD_CHOICE_WORD_COUNT) &&
631
+ !canSegmentChoiceWords(segments, 0, HIGH_CHOICE_WORD_COUNT)) {
632
+ return null;
633
+ }
634
+ return body;
635
+ }
636
+ function canSegmentChoiceWords(segments, segmentIndex, wordsRemaining) {
637
+ if (wordsRemaining === 0)
638
+ return segmentIndex === segments.length;
639
+ const segmentsRemaining = segments.length - segmentIndex;
640
+ if (segmentsRemaining < wordsRemaining)
641
+ return false;
642
+ for (let end = segmentIndex + 1; end <= segments.length - (wordsRemaining - 1); end += 1) {
643
+ const candidate = segments.slice(segmentIndex, end).join("-");
644
+ if (candidate.length > 10)
645
+ break;
646
+ if (isChoiceWord(candidate) && canSegmentChoiceWords(segments, end, wordsRemaining - 1)) {
647
+ return true;
648
+ }
649
+ }
650
+ return false;
651
+ }
652
+ function isServerStoredChoiceRecord(value) {
653
+ if (!value || typeof value !== "object" || Array.isArray(value))
654
+ return false;
655
+ return ("v" in value &&
656
+ value.v === SERVER_STORED_CHOICE_RECORD_VERSION &&
657
+ "storage" in value &&
658
+ value.storage === "server" &&
659
+ "status" in value &&
660
+ (value.status === "active" || value.status === "consumed") &&
661
+ "provider_id" in value &&
662
+ typeof value.provider_id === "string" &&
663
+ "purpose" in value &&
664
+ typeof value.purpose === "string" &&
665
+ "issued_at_ms" in value &&
666
+ typeof value.issued_at_ms === "number" &&
667
+ "ttl_ms" in value &&
668
+ typeof value.ttl_ms === "number" &&
669
+ (!("binding" in value) || value.binding === undefined || isChoiceBinding(value.binding)) &&
670
+ "prefix" in value &&
671
+ typeof value.prefix === "string" &&
672
+ "payload" in value &&
673
+ isChoicePayload(value.payload) &&
674
+ "payload_digest" in value &&
675
+ typeof value.payload_digest === "string" &&
676
+ "replay_key" in value &&
677
+ typeof value.replay_key === "string");
678
+ }
679
+ function wordChoiceNotFoundError() {
680
+ return new ProviderChoiceTokenError("invalid_payload", WORD_CHOICE_NOT_FOUND_MESSAGE);
681
+ }
237
682
  function resolveIssueStorage(storage, payload) {
238
683
  if (!storage || storage.mode === "inline")
239
684
  return { mode: "inline" };
@@ -269,6 +714,10 @@ function resolveChoiceStateNamespace(options) {
269
714
  function stateTtl(storage, ttlMs) {
270
715
  return storage.ttl ?? `${ttlMs ?? 1}ms`;
271
716
  }
717
+ function remainingStateTtl(expiresAt) {
718
+ const remainingMs = Date.parse(expiresAt) - Date.now();
719
+ return `${Number.isFinite(remainingMs) ? Math.max(1, Math.floor(remainingMs)) : 1}ms`;
720
+ }
272
721
  function optionsStateKey(stateId) {
273
722
  return stateId;
274
723
  }
@@ -278,6 +727,9 @@ function serializeChoicePayload(payload) {
278
727
  function digestChoicePayload(serializedPayload) {
279
728
  return createHash("sha256").update(serializedPayload).digest("base64url");
280
729
  }
730
+ function digestChoiceReplayKey(token) {
731
+ return createHash("sha256").update(token).digest("hex");
732
+ }
281
733
  function isServerChoiceHandlePayload(value) {
282
734
  return (value.storage === "server" &&
283
735
  typeof value.state_id === "string" &&
@@ -345,9 +797,7 @@ function isManagedChoiceEnvelope(value) {
345
797
  typeof value.issued_at_ms === "number" &&
346
798
  "ttl_ms" in value &&
347
799
  typeof value.ttl_ms === "number" &&
348
- (!("binding" in value) ||
349
- value.binding === undefined ||
350
- isChoiceBinding(value.binding)));
800
+ (!("binding" in value) || value.binding === undefined || isChoiceBinding(value.binding)));
351
801
  }
352
802
  function isChoicePayload(value) {
353
803
  return Boolean(value && typeof value === "object" && !Array.isArray(value));
@@ -355,29 +805,23 @@ function isChoicePayload(value) {
355
805
  function isChoiceBinding(value) {
356
806
  if (!value || typeof value !== "object" || Array.isArray(value))
357
807
  return false;
358
- return ((!("connection_hash" in value) ||
359
- typeof value.connection_hash === "string") &&
808
+ return ((!("connection_hash" in value) || typeof value.connection_hash === "string") &&
360
809
  (!("credential_hash" in value) || typeof value.credential_hash === "string"));
361
810
  }
362
811
  function assertManagedChoiceEnvelope(envelope, options) {
363
- if (envelope.provider_id !== options.providerId ||
364
- envelope.purpose !== options.purpose) {
812
+ if (envelope.provider_id !== options.providerId || envelope.purpose !== options.purpose) {
365
813
  throw new ProviderChoiceTokenError("invalid_payload", "Provider choice token payload is invalid.");
366
814
  }
367
815
  assertFreshProviderChoiceIssuedAt(envelope.issued_at_ms, {
368
816
  // Clamp to the issuer's embedded TTL so a caller-supplied value cannot
369
817
  // silently extend token validity past the deadline the issuer intended.
370
- ttlMs: options.ttlMs != null
371
- ? Math.min(options.ttlMs, envelope.ttl_ms)
372
- : envelope.ttl_ms,
818
+ ttlMs: options.ttlMs != null ? Math.min(options.ttlMs, envelope.ttl_ms) : envelope.ttl_ms,
373
819
  nowMs: options.nowMs,
374
820
  futureToleranceMs: options.futureToleranceMs,
375
821
  });
376
822
  }
377
823
  function createChoiceBinding(options) {
378
- const connectionHash = options.options?.connection
379
- ? hashRequiredConnection(options)
380
- : undefined;
824
+ const connectionHash = options.options?.connection ? hashRequiredConnection(options) : undefined;
381
825
  const credentialHash = options.options?.credentialKeys?.length
382
826
  ? hashCredentialKeys(options)
383
827
  : undefined;
@@ -388,6 +832,27 @@ function createChoiceBinding(options) {
388
832
  ...(credentialHash ? { credential_hash: credentialHash } : {}),
389
833
  };
390
834
  }
835
+ function hasRequestedChoiceBinding(options) {
836
+ return options?.connection === true || Boolean(options?.credentialKeys?.length);
837
+ }
838
+ function assertWordChoiceBindingMatches(options) {
839
+ const hasStoredBinding = Boolean(options.actual?.connection_hash || options.actual?.credential_hash);
840
+ const hasRequestedBinding = hasRequestedChoiceBinding(options.requested);
841
+ if (!hasStoredBinding && !hasRequestedBinding)
842
+ return;
843
+ if (hasStoredBinding !== hasRequestedBinding)
844
+ throw wordChoiceNotFoundError();
845
+ assertChoiceBindingMatches({
846
+ actual: options.actual,
847
+ expected: createChoiceBinding({
848
+ keys: options.resolveKeys(),
849
+ options: options.requested,
850
+ request: options.request,
851
+ credential: options.credential,
852
+ required: true,
853
+ }),
854
+ });
855
+ }
391
856
  function hashRequiredConnection(options) {
392
857
  const connectionId = options.request?.connectionId;
393
858
  if (!connectionId) {