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

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 +127 -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 +122 -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 +13 -1
  79. package/dist/runtime/choice.js +514 -124
  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 +2077 -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 +127 -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 +157 -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 +65 -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 +685 -216
  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 +3060 -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,48 @@
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";
5
6
  const PRIMARY_CHOICE_TOKEN_KID = "v1";
6
7
  const MANAGED_CHOICE_TOKEN_VERSION = 1;
8
+ const SERVER_STORED_CHOICE_RECORD_VERSION = 1;
9
+ const SERVER_STORED_CHOICE_ISSUE_ATTEMPTS = 5;
10
+ const WORD_CHOICE_NOT_FOUND_MESSAGE = "Provider choice token was not found.";
7
11
  export function createProviderChoiceContext(options) {
8
12
  const kid = options.kid ?? PRIMARY_CHOICE_TOKEN_KID;
9
13
  const resolveMasterSecret = () => resolveChoiceMasterSecret(options);
10
14
  function issue(issueOptions) {
11
15
  const issuedAtMs = issueOptions.nowMs ?? Date.now();
16
+ const resolvedStorage = resolveIssueStorage(issueOptions.storage, issueOptions.payload);
17
+ if (resolvedStorage.mode === "server") {
18
+ const binding = hasRequestedChoiceBinding(issueOptions.bind)
19
+ ? createChoiceBinding({
20
+ keys: deriveManagedChoiceKeys({
21
+ masterSecret: resolveMasterSecret(),
22
+ providerId: options.providerId,
23
+ purpose: issueOptions.purpose,
24
+ kid,
25
+ }),
26
+ options: issueOptions.bind,
27
+ request: options.request,
28
+ credential: options.credential,
29
+ required: true,
30
+ })
31
+ : undefined;
32
+ return issueServerStoredChoice({
33
+ baseEnvelope: {
34
+ v: MANAGED_CHOICE_TOKEN_VERSION,
35
+ provider_id: options.providerId,
36
+ purpose: issueOptions.purpose,
37
+ issued_at_ms: issuedAtMs,
38
+ ttl_ms: issueOptions.ttlMs,
39
+ binding,
40
+ },
41
+ issueOptions,
42
+ storage: resolvedStorage.storage,
43
+ contextState: options.state,
44
+ });
45
+ }
12
46
  const keys = deriveManagedChoiceKeys({
13
47
  masterSecret: resolveMasterSecret(),
14
48
  providerId: options.providerId,
@@ -29,18 +63,6 @@ export function createProviderChoiceContext(options) {
29
63
  required: true,
30
64
  }),
31
65
  };
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
66
  const envelope = {
45
67
  ...baseEnvelope,
46
68
  payload: issueOptions.payload,
@@ -53,77 +75,220 @@ export function createProviderChoiceContext(options) {
53
75
  });
54
76
  }
55
77
  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,
70
- });
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,
78
+ const consumeMode = parseOptions.consume ?? "never";
79
+ const wordStateKey = parseWordChoiceStateKey({
80
+ token: parseOptions.token,
81
+ prefix: parseOptions.prefix,
95
82
  });
96
- assertChoiceBindingMatches({
97
- actual: envelope.binding,
98
- expected: createChoiceBinding({
99
- keys,
100
- options: parseOptions.bind,
83
+ if (wordStateKey) {
84
+ const parsed = parseWordServerStoredChoice({
85
+ stateKey: wordStateKey,
86
+ parseOptions,
87
+ contextState: options.state,
88
+ providerId: options.providerId,
101
89
  request: options.request,
102
90
  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,
91
+ resolveBindingKeys: () => deriveManagedChoiceKeys({
92
+ masterSecret: resolveMasterSecret(),
93
+ providerId: options.providerId,
94
+ purpose: parseOptions.purpose,
95
+ kid,
96
+ }),
97
+ onConsume: (result) => emitChoiceTelemetry(options.onTelemetry, {
98
+ providerId: options.providerId,
99
+ purpose: parseOptions.purpose,
100
+ operation: "consume",
101
+ format: "word",
102
+ outcome: "success",
103
+ consumeMode,
104
+ consumed: result.status === "consumed",
105
+ replay: result.status === "already-consumed",
106
+ }),
107
+ });
108
+ return observeChoiceParse(parsed, {
109
+ onTelemetry: options.onTelemetry,
110
+ providerId: options.providerId,
111
+ purpose: parseOptions.purpose,
112
+ format: "word",
113
+ consumeMode,
111
114
  });
112
115
  }
113
- return envelope.payload;
116
+ // Legacy encrypted-envelope compatibility fallback. Removal is gated on
117
+ // the last legacy mint plus the maximum issued TTL; see ADR 0006.
118
+ // A structurally valid word token returns above, so lookup, expiry,
119
+ // consumption, and binding failures can never enter this branch.
120
+ try {
121
+ const [actualPrefix, tokenKid, encodedIv, encryptedPayload, authTag, signature] = parseManagedChoiceTokenParts(parseOptions.token);
122
+ if (actualPrefix !== parseOptions.prefix ||
123
+ tokenKid !== kid ||
124
+ !encodedIv ||
125
+ !encryptedPayload ||
126
+ !authTag ||
127
+ !signature) {
128
+ throw new ProviderChoiceTokenError("invalid_shape", "Provider choice token shape is invalid.");
129
+ }
130
+ const keys = deriveManagedChoiceKeys({
131
+ masterSecret: resolveMasterSecret(),
132
+ providerId: options.providerId,
133
+ purpose: parseOptions.purpose,
134
+ kid: tokenKid,
135
+ });
136
+ const signedBody = [
137
+ parseOptions.prefix,
138
+ tokenKid,
139
+ encodedIv,
140
+ encryptedPayload,
141
+ authTag,
142
+ ].join(".");
143
+ assertManagedChoiceSignature({
144
+ signedBody,
145
+ signature,
146
+ signingKey: keys.signing,
147
+ });
148
+ const envelope = decryptManagedChoiceToken({
149
+ encodedIv,
150
+ encryptedPayload,
151
+ authTag,
152
+ encryptionKey: keys.encryption,
153
+ });
154
+ assertManagedChoiceEnvelope(envelope, {
155
+ providerId: options.providerId,
156
+ purpose: parseOptions.purpose,
157
+ ttlMs: parseOptions.ttlMs,
158
+ nowMs: parseOptions.nowMs,
159
+ futureToleranceMs: parseOptions.futureToleranceMs,
160
+ });
161
+ assertChoiceBindingMatches({
162
+ actual: envelope.binding,
163
+ expected: createChoiceBinding({
164
+ keys,
165
+ options: parseOptions.bind,
166
+ request: options.request,
167
+ credential: options.credential,
168
+ required: true,
169
+ }),
170
+ });
171
+ const payload = isServerChoiceHandlePayload(envelope.payload)
172
+ ? parseLegacyServerStoredChoice({
173
+ handle: envelope.payload,
174
+ storage: parseOptions.storage,
175
+ contextState: options.state,
176
+ })
177
+ : envelope.payload;
178
+ const parsed = consumeMode === "explicit"
179
+ ? Promise.resolve(payload).then((resolvedPayload) => createLegacyExplicitParseResult({
180
+ payload: resolvedPayload,
181
+ replayKey: digestChoiceReplayKey(parseOptions.token),
182
+ onConsume: () => emitChoiceTelemetry(options.onTelemetry, {
183
+ providerId: options.providerId,
184
+ purpose: parseOptions.purpose,
185
+ operation: "consume",
186
+ format: "legacy",
187
+ outcome: "unsupported",
188
+ consumeMode,
189
+ consumed: false,
190
+ replay: false,
191
+ }),
192
+ }))
193
+ : payload;
194
+ return observeChoiceParse(parsed, {
195
+ onTelemetry: options.onTelemetry,
196
+ providerId: options.providerId,
197
+ purpose: parseOptions.purpose,
198
+ format: "legacy",
199
+ consumeMode,
200
+ });
201
+ }
202
+ catch (error) {
203
+ emitChoiceParseFailure(options.onTelemetry, error, {
204
+ providerId: options.providerId,
205
+ purpose: parseOptions.purpose,
206
+ format: "legacy",
207
+ consumeMode,
208
+ });
209
+ throw error;
210
+ }
114
211
  }
115
212
  return { issue, parse };
116
213
  }
117
214
  export function createTestProviderChoiceContext(options) {
118
215
  return createProviderChoiceContext({
119
216
  ...options,
120
- masterSecret: options.masterSecret ??
121
- "apifuse-test-provider-runtime-choice-token-master-secret",
217
+ masterSecret: options.masterSecret ?? "apifuse-test-provider-runtime-choice-token-master-secret",
218
+ });
219
+ }
220
+ function observeChoiceParse(result, base) {
221
+ if (result instanceof Promise) {
222
+ return result.then((value) => {
223
+ emitChoiceParseSuccess(base, value);
224
+ return value;
225
+ }, (error) => {
226
+ emitChoiceParseFailure(base.onTelemetry, error, base);
227
+ throw error;
228
+ });
229
+ }
230
+ emitChoiceParseSuccess(base, result);
231
+ return result;
232
+ }
233
+ function emitChoiceParseSuccess(base, result) {
234
+ const replay = isConsumedChoiceReplay(result);
235
+ emitChoiceTelemetry(base.onTelemetry, {
236
+ providerId: base.providerId,
237
+ purpose: base.purpose,
238
+ operation: "parse",
239
+ format: base.format,
240
+ outcome: "success",
241
+ consumeMode: base.consumeMode,
242
+ consumed: replay || (base.format === "word" && base.consumeMode === "on-parse"),
243
+ replay,
122
244
  });
123
245
  }
246
+ function isConsumedChoiceReplay(value) {
247
+ return (value !== null &&
248
+ typeof value === "object" &&
249
+ "status" in value &&
250
+ value.status === "consumed" &&
251
+ "replayKey" in value &&
252
+ typeof value.replayKey === "string");
253
+ }
254
+ function emitChoiceParseFailure(onTelemetry, error, base) {
255
+ const outcome = error instanceof ProviderChoiceTokenError
256
+ ? base.format === "word" && error.message === WORD_CHOICE_NOT_FOUND_MESSAGE
257
+ ? "not-found"
258
+ : "invalid"
259
+ : "error";
260
+ emitChoiceTelemetry(onTelemetry, {
261
+ providerId: base.providerId,
262
+ purpose: base.purpose,
263
+ operation: "parse",
264
+ format: base.format,
265
+ outcome,
266
+ consumeMode: base.consumeMode,
267
+ consumed: false,
268
+ replay: false,
269
+ });
270
+ }
271
+ function emitChoiceTelemetry(onTelemetry, event) {
272
+ try {
273
+ onTelemetry?.(event);
274
+ }
275
+ catch {
276
+ // Observability must never change provider token semantics.
277
+ }
278
+ }
279
+ function createLegacyExplicitParseResult(options) {
280
+ return {
281
+ status: "active",
282
+ payload: options.payload,
283
+ replayKey: options.replayKey,
284
+ consume: async () => {
285
+ options.onConsume();
286
+ return { status: "unsupported" };
287
+ },
288
+ };
289
+ }
124
290
  function resolveChoiceMasterSecret(options) {
125
- const configured = options.masterSecret ??
126
- options.env?.get(PROVIDER_RUNTIME_CHOICE_TOKEN_MASTER_SECRET_ENV);
291
+ const configured = options.masterSecret ?? options.env?.get(PROVIDER_RUNTIME_CHOICE_TOKEN_MASTER_SECRET_ENV);
127
292
  const trimmed = configured?.trim();
128
293
  if (trimmed)
129
294
  return trimmed;
@@ -165,13 +330,7 @@ function encryptManagedChoiceToken(options) {
165
330
  ]).toString("base64url");
166
331
  const authTag = cipher.getAuthTag().toString("base64url");
167
332
  const encodedIv = iv.toString("base64url");
168
- const signedBody = [
169
- options.prefix,
170
- options.kid,
171
- encodedIv,
172
- encryptedPayload,
173
- authTag,
174
- ].join(".");
333
+ const signedBody = [options.prefix, options.kid, encodedIv, encryptedPayload, authTag].join(".");
175
334
  const signature = createHmac("sha256", options.keys.signing)
176
335
  .update(signedBody)
177
336
  .digest("base64url");
@@ -179,51 +338,197 @@ function encryptManagedChoiceToken(options) {
179
338
  }
180
339
  async function issueServerStoredChoice(options) {
181
340
  const serializedPayload = serializeChoicePayload(options.issueOptions.payload);
182
- const payloadBytes = Buffer.byteLength(serializedPayload, "utf8");
183
- if (payloadBytes > options.storage.maxValueBytes) {
184
- throw new ProviderError("Provider choice payload exceeds state storage policy.", {
185
- code: "CHOICE_STATE_PAYLOAD_TOO_LARGE",
186
- category: "input_validation",
187
- retryable: false,
188
- details: {
189
- maxValueBytes: options.storage.maxValueBytes,
190
- payloadBytes,
191
- },
192
- });
193
- }
194
- const stateId = `choice_${randomBytes(16).toString("base64url")}`;
195
- const digest = digestChoicePayload(serializedPayload);
341
+ const payloadDigest = digestChoicePayload(serializedPayload);
196
342
  const namespace = resolveChoiceStateNamespace({
197
343
  storage: options.storage,
198
344
  contextState: options.contextState,
199
345
  ttlMs: options.issueOptions.ttlMs,
200
346
  });
201
- await namespace.set(optionsStateKey(stateId), options.issueOptions.payload, {
202
- ttl: stateTtl(options.storage, options.issueOptions.ttlMs),
203
- });
204
- const envelope = {
205
- ...options.baseEnvelope,
206
- payload: {
347
+ const wordCount = options.issueOptions.strength === "high" ? HIGH_CHOICE_WORD_COUNT : STANDARD_CHOICE_WORD_COUNT;
348
+ for (let attempt = 0; attempt < SERVER_STORED_CHOICE_ISSUE_ATTEMPTS; attempt += 1) {
349
+ const stateKey = generateChoiceWordSequence(wordCount);
350
+ const token = `${options.issueOptions.prefix}${stateKey}`;
351
+ const record = {
352
+ v: SERVER_STORED_CHOICE_RECORD_VERSION,
207
353
  storage: "server",
208
- state_id: stateId,
209
- payload_digest: digest,
210
- created_at_ms: options.issuedAtMs,
211
- },
212
- };
213
- return encryptManagedChoiceToken({
214
- prefix: options.issueOptions.prefix,
215
- kid: options.kid,
216
- envelope,
217
- keys: options.keys,
354
+ status: "active",
355
+ provider_id: options.baseEnvelope.provider_id,
356
+ purpose: options.baseEnvelope.purpose,
357
+ issued_at_ms: options.baseEnvelope.issued_at_ms,
358
+ ttl_ms: options.baseEnvelope.ttl_ms,
359
+ binding: options.baseEnvelope.binding,
360
+ prefix: options.issueOptions.prefix,
361
+ payload: options.issueOptions.payload,
362
+ payload_digest: payloadDigest,
363
+ replay_key: digestChoiceReplayKey(token),
364
+ };
365
+ const valueBytes = Buffer.byteLength(JSON.stringify(record), "utf8");
366
+ if (valueBytes > options.storage.maxValueBytes) {
367
+ throw new ProviderError("Provider choice payload exceeds state storage policy.", {
368
+ code: "CHOICE_STATE_PAYLOAD_TOO_LARGE",
369
+ category: "input_validation",
370
+ retryable: false,
371
+ details: {
372
+ maxValueBytes: options.storage.maxValueBytes,
373
+ valueBytes,
374
+ },
375
+ });
376
+ }
377
+ const result = await namespace.compareAndSet(optionsStateKey(stateKey), 0, record, {
378
+ ttl: stateTtl(options.storage, options.issueOptions.ttlMs),
379
+ });
380
+ if (result.ok)
381
+ return token;
382
+ }
383
+ throw new ProviderError("Provider choice state storage is not available.", {
384
+ code: "CHOICE_STATE_UNAVAILABLE",
385
+ category: "internal_error",
386
+ retryable: false,
387
+ });
388
+ }
389
+ async function parseWordServerStoredChoice(options) {
390
+ const storage = resolveParseStorage(options.parseOptions.storage);
391
+ const namespace = resolveChoiceStateNamespace({
392
+ storage,
393
+ contextState: options.contextState,
394
+ ttlMs: options.parseOptions.ttlMs,
218
395
  });
396
+ let stored;
397
+ try {
398
+ stored = await namespace.get(optionsStateKey(options.stateKey));
399
+ }
400
+ catch (error) {
401
+ if (isProviderError(error))
402
+ throw error;
403
+ throw wordChoiceNotFoundError();
404
+ }
405
+ if (!stored || !isServerStoredChoiceRecord(stored.value)) {
406
+ throw wordChoiceNotFoundError();
407
+ }
408
+ const record = stored.value;
409
+ const expectedReplayKey = digestChoiceReplayKey(`${options.parseOptions.prefix}${options.stateKey}`);
410
+ try {
411
+ if (record.provider_id !== options.providerId ||
412
+ record.purpose !== options.parseOptions.purpose ||
413
+ record.prefix !== options.parseOptions.prefix) {
414
+ throw wordChoiceNotFoundError();
415
+ }
416
+ assertFreshProviderChoiceIssuedAt(record.issued_at_ms, {
417
+ ttlMs: options.parseOptions.ttlMs != null
418
+ ? Math.min(options.parseOptions.ttlMs, record.ttl_ms)
419
+ : record.ttl_ms,
420
+ nowMs: options.parseOptions.nowMs,
421
+ futureToleranceMs: options.parseOptions.futureToleranceMs,
422
+ });
423
+ assertPayloadDigestMatches({
424
+ actual: digestChoicePayload(serializeChoicePayload(record.payload)),
425
+ expected: record.payload_digest,
426
+ });
427
+ assertPayloadDigestMatches({ actual: expectedReplayKey, expected: record.replay_key });
428
+ assertWordChoiceBindingMatches({
429
+ actual: record.binding,
430
+ requested: options.parseOptions.bind,
431
+ request: options.request,
432
+ credential: options.credential,
433
+ resolveKeys: options.resolveBindingKeys,
434
+ });
435
+ }
436
+ catch (error) {
437
+ if (error instanceof ProviderChoiceTokenError ||
438
+ (isProviderError(error) && error.code === "CHOICE_CONTEXT_REQUIRED")) {
439
+ throw wordChoiceNotFoundError();
440
+ }
441
+ throw error;
442
+ }
443
+ const consumeMode = options.parseOptions.consume ?? "never";
444
+ if (record.status === "consumed") {
445
+ if (consumeMode === "explicit") {
446
+ return { status: "consumed", replayKey: record.replay_key };
447
+ }
448
+ throw wordChoiceNotFoundError();
449
+ }
450
+ if (consumeMode === "never")
451
+ return record.payload;
452
+ if (consumeMode === "explicit") {
453
+ return {
454
+ status: "active",
455
+ payload: record.payload,
456
+ replayKey: record.replay_key,
457
+ consume: async () => {
458
+ const result = await consumeWordServerStoredChoice({
459
+ stateKey: options.stateKey,
460
+ stored,
461
+ record,
462
+ storage,
463
+ contextState: options.contextState,
464
+ });
465
+ options.onConsume(result);
466
+ return result;
467
+ },
468
+ };
469
+ }
470
+ const consumed = await consumeWordServerStoredChoice({
471
+ stateKey: options.stateKey,
472
+ stored,
473
+ record,
474
+ storage,
475
+ contextState: options.contextState,
476
+ });
477
+ if (consumed.status !== "consumed")
478
+ throw wordChoiceNotFoundError();
479
+ return record.payload;
480
+ }
481
+ async function consumeWordServerStoredChoice(options) {
482
+ const namespace = resolveChoiceStateNamespace({
483
+ storage: options.storage,
484
+ contextState: options.contextState,
485
+ ttlMs: options.record.ttl_ms,
486
+ });
487
+ try {
488
+ const consumed = await namespace.compareAndSet(optionsStateKey(options.stateKey), options.stored.version, { ...options.record, status: "consumed" }, { ttl: remainingStateTtl(options.stored.expiresAt) });
489
+ if (consumed.ok)
490
+ return { status: "consumed" };
491
+ if (consumed.current &&
492
+ isServerStoredChoiceRecord(consumed.current.value) &&
493
+ consumed.current.value.status === "consumed" &&
494
+ consumed.current.value.replay_key === options.record.replay_key) {
495
+ return { status: "already-consumed" };
496
+ }
497
+ throw wordChoiceNotFoundError();
498
+ }
499
+ catch (error) {
500
+ if (isProviderError(error))
501
+ throw error;
502
+ if (error instanceof ProviderChoiceTokenError)
503
+ throw error;
504
+ throw wordChoiceNotFoundError();
505
+ }
219
506
  }
220
- async function parseServerStoredChoice(options) {
507
+ async function parseLegacyServerStoredChoice(options) {
221
508
  const storage = resolveParseStorage(options.storage);
222
509
  const namespace = resolveChoiceStateNamespace({
223
510
  storage,
224
511
  contextState: options.contextState,
225
512
  });
226
- const record = await namespace.get(optionsStateKey(options.handle.state_id));
513
+ // Reading a server-stored choice back deserializes a persisted value. A
514
+ // corrupt/undecodable value would otherwise surface as a raw JSON.parse
515
+ // SyntaxError (or another unexpected throwable) that escapes the choice error
516
+ // taxonomy, gets masked as internal_error 500, and is treated as retryable by
517
+ // the hub -> reservation restart loop (2026-07-22 catchtable RCA, candidate A).
518
+ // Convert any non-branded throwable into a branded invalid_payload so it maps
519
+ // to a clean, non-retryable 400. Branded ProviderChoiceTokenError and genuine
520
+ // ProviderError (e.g. Redis-unavailable / state-unavailable) pass through so
521
+ // their category/retryable semantics are preserved.
522
+ let record;
523
+ try {
524
+ record = await namespace.get(optionsStateKey(options.handle.state_id));
525
+ }
526
+ catch (error) {
527
+ if (error instanceof ProviderChoiceTokenError || isProviderError(error)) {
528
+ throw error;
529
+ }
530
+ throw new ProviderChoiceTokenError("invalid_payload", "Provider choice token state payload could not be decoded.");
531
+ }
227
532
  if (!record) {
228
533
  throw new ProviderChoiceTokenError("invalid_payload", "Provider choice token state payload is missing.");
229
534
  }
@@ -234,6 +539,71 @@ async function parseServerStoredChoice(options) {
234
539
  });
235
540
  return record.value;
236
541
  }
542
+ function generateChoiceWordSequence(wordCount) {
543
+ return Array.from({ length: wordCount }, () => choiceWordAt(randomInt(CHOICE_WORDLIST_SIZE))).join("-");
544
+ }
545
+ function parseWordChoiceStateKey(options) {
546
+ if (!options.token.startsWith(options.prefix))
547
+ return null;
548
+ const body = options.token.slice(options.prefix.length);
549
+ // The official list contains one hyphenated entry (`yo-yo`), so structural
550
+ // recognition uses dictionary-aware segmentation instead of assuming every
551
+ // hyphen is a word boundary.
552
+ if (!/^[a-z]+(?:-[a-z]+){3,9}$/.test(body))
553
+ return null;
554
+ const segments = body.split("-");
555
+ if (!canSegmentChoiceWords(segments, 0, STANDARD_CHOICE_WORD_COUNT) &&
556
+ !canSegmentChoiceWords(segments, 0, HIGH_CHOICE_WORD_COUNT)) {
557
+ return null;
558
+ }
559
+ return body;
560
+ }
561
+ function canSegmentChoiceWords(segments, segmentIndex, wordsRemaining) {
562
+ if (wordsRemaining === 0)
563
+ return segmentIndex === segments.length;
564
+ const segmentsRemaining = segments.length - segmentIndex;
565
+ if (segmentsRemaining < wordsRemaining)
566
+ return false;
567
+ for (let end = segmentIndex + 1; end <= segments.length - (wordsRemaining - 1); end += 1) {
568
+ const candidate = segments.slice(segmentIndex, end).join("-");
569
+ if (candidate.length > 10)
570
+ break;
571
+ if (isChoiceWord(candidate) && canSegmentChoiceWords(segments, end, wordsRemaining - 1)) {
572
+ return true;
573
+ }
574
+ }
575
+ return false;
576
+ }
577
+ function isServerStoredChoiceRecord(value) {
578
+ if (!value || typeof value !== "object" || Array.isArray(value))
579
+ return false;
580
+ return ("v" in value &&
581
+ value.v === SERVER_STORED_CHOICE_RECORD_VERSION &&
582
+ "storage" in value &&
583
+ value.storage === "server" &&
584
+ "status" in value &&
585
+ (value.status === "active" || value.status === "consumed") &&
586
+ "provider_id" in value &&
587
+ typeof value.provider_id === "string" &&
588
+ "purpose" in value &&
589
+ typeof value.purpose === "string" &&
590
+ "issued_at_ms" in value &&
591
+ typeof value.issued_at_ms === "number" &&
592
+ "ttl_ms" in value &&
593
+ typeof value.ttl_ms === "number" &&
594
+ (!("binding" in value) || value.binding === undefined || isChoiceBinding(value.binding)) &&
595
+ "prefix" in value &&
596
+ typeof value.prefix === "string" &&
597
+ "payload" in value &&
598
+ isChoicePayload(value.payload) &&
599
+ "payload_digest" in value &&
600
+ typeof value.payload_digest === "string" &&
601
+ "replay_key" in value &&
602
+ typeof value.replay_key === "string");
603
+ }
604
+ function wordChoiceNotFoundError() {
605
+ return new ProviderChoiceTokenError("invalid_payload", WORD_CHOICE_NOT_FOUND_MESSAGE);
606
+ }
237
607
  function resolveIssueStorage(storage, payload) {
238
608
  if (!storage || storage.mode === "inline")
239
609
  return { mode: "inline" };
@@ -269,6 +639,10 @@ function resolveChoiceStateNamespace(options) {
269
639
  function stateTtl(storage, ttlMs) {
270
640
  return storage.ttl ?? `${ttlMs ?? 1}ms`;
271
641
  }
642
+ function remainingStateTtl(expiresAt) {
643
+ const remainingMs = Date.parse(expiresAt) - Date.now();
644
+ return `${Number.isFinite(remainingMs) ? Math.max(1, Math.floor(remainingMs)) : 1}ms`;
645
+ }
272
646
  function optionsStateKey(stateId) {
273
647
  return stateId;
274
648
  }
@@ -278,6 +652,9 @@ function serializeChoicePayload(payload) {
278
652
  function digestChoicePayload(serializedPayload) {
279
653
  return createHash("sha256").update(serializedPayload).digest("base64url");
280
654
  }
655
+ function digestChoiceReplayKey(token) {
656
+ return createHash("sha256").update(token).digest("hex");
657
+ }
281
658
  function isServerChoiceHandlePayload(value) {
282
659
  return (value.storage === "server" &&
283
660
  typeof value.state_id === "string" &&
@@ -345,9 +722,7 @@ function isManagedChoiceEnvelope(value) {
345
722
  typeof value.issued_at_ms === "number" &&
346
723
  "ttl_ms" in value &&
347
724
  typeof value.ttl_ms === "number" &&
348
- (!("binding" in value) ||
349
- value.binding === undefined ||
350
- isChoiceBinding(value.binding)));
725
+ (!("binding" in value) || value.binding === undefined || isChoiceBinding(value.binding)));
351
726
  }
352
727
  function isChoicePayload(value) {
353
728
  return Boolean(value && typeof value === "object" && !Array.isArray(value));
@@ -355,29 +730,23 @@ function isChoicePayload(value) {
355
730
  function isChoiceBinding(value) {
356
731
  if (!value || typeof value !== "object" || Array.isArray(value))
357
732
  return false;
358
- return ((!("connection_hash" in value) ||
359
- typeof value.connection_hash === "string") &&
733
+ return ((!("connection_hash" in value) || typeof value.connection_hash === "string") &&
360
734
  (!("credential_hash" in value) || typeof value.credential_hash === "string"));
361
735
  }
362
736
  function assertManagedChoiceEnvelope(envelope, options) {
363
- if (envelope.provider_id !== options.providerId ||
364
- envelope.purpose !== options.purpose) {
737
+ if (envelope.provider_id !== options.providerId || envelope.purpose !== options.purpose) {
365
738
  throw new ProviderChoiceTokenError("invalid_payload", "Provider choice token payload is invalid.");
366
739
  }
367
740
  assertFreshProviderChoiceIssuedAt(envelope.issued_at_ms, {
368
741
  // Clamp to the issuer's embedded TTL so a caller-supplied value cannot
369
742
  // 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,
743
+ ttlMs: options.ttlMs != null ? Math.min(options.ttlMs, envelope.ttl_ms) : envelope.ttl_ms,
373
744
  nowMs: options.nowMs,
374
745
  futureToleranceMs: options.futureToleranceMs,
375
746
  });
376
747
  }
377
748
  function createChoiceBinding(options) {
378
- const connectionHash = options.options?.connection
379
- ? hashRequiredConnection(options)
380
- : undefined;
749
+ const connectionHash = options.options?.connection ? hashRequiredConnection(options) : undefined;
381
750
  const credentialHash = options.options?.credentialKeys?.length
382
751
  ? hashCredentialKeys(options)
383
752
  : undefined;
@@ -388,6 +757,27 @@ function createChoiceBinding(options) {
388
757
  ...(credentialHash ? { credential_hash: credentialHash } : {}),
389
758
  };
390
759
  }
760
+ function hasRequestedChoiceBinding(options) {
761
+ return options?.connection === true || Boolean(options?.credentialKeys?.length);
762
+ }
763
+ function assertWordChoiceBindingMatches(options) {
764
+ const hasStoredBinding = Boolean(options.actual?.connection_hash || options.actual?.credential_hash);
765
+ const hasRequestedBinding = hasRequestedChoiceBinding(options.requested);
766
+ if (!hasStoredBinding && !hasRequestedBinding)
767
+ return;
768
+ if (hasStoredBinding !== hasRequestedBinding)
769
+ throw wordChoiceNotFoundError();
770
+ assertChoiceBindingMatches({
771
+ actual: options.actual,
772
+ expected: createChoiceBinding({
773
+ keys: options.resolveKeys(),
774
+ options: options.requested,
775
+ request: options.request,
776
+ credential: options.credential,
777
+ required: true,
778
+ }),
779
+ });
780
+ }
391
781
  function hashRequiredConnection(options) {
392
782
  const connectionId = options.request?.connectionId;
393
783
  if (!connectionId) {