@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.
- package/AUTHORING.md +493 -5
- package/CHANGELOG.md +131 -1
- package/README.md +52 -6
- package/SUBMISSION.md +1 -1
- package/bin/apifuse-check.ts +106 -62
- package/bin/apifuse-create.ts +1 -1
- package/bin/apifuse-dev.ts +63 -55
- package/bin/apifuse-pack-check.ts +22 -2
- package/bin/apifuse-pack-smoke.ts +78 -82
- package/bin/apifuse-pack-types.ts +583 -0
- package/bin/apifuse-perf.ts +59 -140
- package/bin/apifuse-record.ts +698 -113
- package/bin/apifuse-submit-check.ts +517 -44
- package/bin/apifuse-sync-assets.ts +117 -0
- package/bin/apifuse.ts +1 -1
- package/bin/submit-check-delimited-text.ts +50 -0
- package/bin/submit-check-xml.ts +1 -1
- package/dist/auth-turn/index.d.ts +4 -4
- package/dist/auth-turn/index.js +1 -1
- package/dist/auth.d.ts +16 -2
- package/dist/auth.js +76 -18
- package/dist/ceremonies/index.d.ts +9 -1
- package/dist/ceremonies/index.js +117 -29
- package/dist/cli/commands.d.ts +1 -1
- package/dist/cli/commands.js +8 -0
- package/dist/cli/create.d.ts +3 -0
- package/dist/cli/create.js +34 -35
- package/dist/cli/prompt-assets.d.ts +80 -0
- package/dist/cli/prompt-assets.js +743 -0
- package/dist/cli/templates/provider/AGENTS.md.tpl +17 -8
- package/dist/cli/templates/provider/README.md.tpl +4 -4
- package/dist/config/loader.d.ts +176 -17
- package/dist/config/loader.js +434 -161
- package/dist/contract-serialization.d.ts +2 -2
- package/dist/contract-serialization.js +7 -14
- package/dist/contract-types.d.ts +3 -2
- package/dist/contract.d.ts +3 -3
- package/dist/contract.js +6 -6
- package/dist/declaration-validation.d.ts +23 -0
- package/dist/declaration-validation.js +159 -0
- package/dist/define.d.ts +13 -1
- package/dist/define.js +391 -122
- package/dist/dev.d.ts +1 -1
- package/dist/dev.js +1 -1
- package/dist/error-resolution.d.ts +4 -0
- package/dist/error-resolution.js +123 -0
- package/dist/errors.d.ts +19 -1
- package/dist/errors.js +41 -3
- package/dist/fixture-sanitization.d.ts +26 -0
- package/dist/fixture-sanitization.js +216 -0
- package/dist/i18n/catalog.d.ts +2 -2
- package/dist/i18n/catalog.js +4 -10
- package/dist/i18n/index.d.ts +2 -2
- package/dist/i18n/index.js +2 -2
- package/dist/i18n/keys.d.ts +2 -2
- package/dist/index.d.ts +50 -42
- package/dist/index.js +41 -37
- package/dist/lint.d.ts +6 -1
- package/dist/lint.js +370 -18
- package/dist/native-address.d.ts +43 -0
- package/dist/native-address.js +281 -0
- package/dist/native-egress-policy.d.ts +31 -0
- package/dist/native-egress-policy.js +288 -0
- package/dist/observability.d.ts +5 -2
- package/dist/observability.js +48 -1
- package/dist/provider.d.ts +13 -11
- package/dist/provider.js +10 -9
- package/dist/public-schema-field-lint.d.ts +1 -1
- package/dist/recipes/gov-api.js +1 -1
- package/dist/runtime/auth-flow.d.ts +3 -1
- package/dist/runtime/auth-flow.js +8 -3
- package/dist/runtime/browser.d.ts +1 -1
- package/dist/runtime/browser.js +138 -40
- package/dist/runtime/cache.d.ts +2 -1
- package/dist/runtime/cache.js +173 -23
- package/dist/runtime/choice-wordlist.d.ts +9 -0
- package/dist/runtime/choice-wordlist.js +138 -0
- package/dist/runtime/choice.d.ts +14 -1
- package/dist/runtime/choice.js +566 -101
- package/dist/runtime/credential.d.ts +1 -1
- package/dist/runtime/credential.js +1 -1
- package/dist/runtime/env.d.ts +1 -1
- package/dist/runtime/executor.d.ts +1 -1
- package/dist/runtime/executor.js +25 -3
- package/dist/runtime/http.d.ts +3 -2
- package/dist/runtime/http.js +517 -55
- package/dist/runtime/insights.d.ts +1 -1
- package/dist/runtime/insights.js +6 -13
- package/dist/runtime/instrumentation.d.ts +2 -2
- package/dist/runtime/instrumentation.js +371 -23
- package/dist/runtime/keyring.js +1 -1
- package/dist/runtime/namespace.js +1 -1
- package/dist/runtime/native-network-errors.d.ts +33 -0
- package/dist/runtime/native-network-errors.js +69 -0
- package/dist/runtime/native-network.d.ts +96 -0
- package/dist/runtime/native-network.js +1232 -0
- package/dist/runtime/ocr.d.ts +29 -0
- package/dist/runtime/ocr.js +440 -0
- package/dist/runtime/otlp.d.ts +1 -1
- package/dist/runtime/perf.d.ts +1 -1
- package/dist/runtime/provider.d.ts +1 -1
- package/dist/runtime/provider.js +1 -2
- package/dist/runtime/proxy-errors.d.ts +1 -1
- package/dist/runtime/proxy-errors.js +9 -7
- package/dist/runtime/proxy-nodemaven.d.ts +56 -0
- package/dist/runtime/proxy-nodemaven.js +146 -0
- package/dist/runtime/proxy-retry-policy.d.ts +2 -2
- package/dist/runtime/proxy-retry-policy.js +2 -2
- package/dist/runtime/proxy-telemetry.d.ts +2 -1
- package/dist/runtime/proxy-telemetry.js +58 -52
- package/dist/runtime/redirects.d.ts +29 -0
- package/dist/runtime/redirects.js +36 -0
- package/dist/runtime/redis.d.ts +1 -1
- package/dist/runtime/redis.js +5 -5
- package/dist/runtime/request-options.d.ts +68 -1
- package/dist/runtime/request-options.js +548 -0
- package/dist/runtime/resolver-config.d.ts +6 -0
- package/dist/runtime/resolver-config.js +6 -0
- package/dist/runtime/resolver-public.d.ts +1 -0
- package/dist/runtime/resolver-public.js +1 -0
- package/dist/runtime/resolver-shared.d.ts +3 -0
- package/dist/runtime/resolver-shared.js +12 -0
- package/dist/runtime/resolver-vendors/bindings.d.ts +48 -0
- package/dist/runtime/resolver-vendors/bindings.js +40 -0
- package/dist/runtime/resolver-vendors/browser.d.ts +20 -0
- package/dist/runtime/resolver-vendors/browser.js +282 -0
- package/dist/runtime/resolver-vendors/hosts.d.ts +2 -0
- package/dist/runtime/resolver-vendors/hosts.js +33 -0
- package/dist/runtime/resolver-vendors/twocaptcha.d.ts +24 -0
- package/dist/runtime/resolver-vendors/twocaptcha.js +368 -0
- package/dist/runtime/resolver-vendors/types.d.ts +83 -0
- package/dist/runtime/resolver-vendors/types.js +69 -0
- package/dist/runtime/resolver.d.ts +59 -0
- package/dist/runtime/resolver.js +705 -0
- package/dist/runtime/secrets.d.ts +27 -0
- package/dist/runtime/secrets.js +51 -0
- package/dist/runtime/state.d.ts +5 -2
- package/dist/runtime/state.js +280 -74
- package/dist/runtime/stealth-cookies.d.ts +20 -0
- package/dist/runtime/stealth-cookies.js +111 -0
- package/dist/runtime/stealth.d.ts +30 -5
- package/dist/runtime/stealth.js +523 -259
- package/dist/runtime/stt.d.ts +1 -1
- package/dist/runtime/stt.js +12 -27
- package/dist/runtime/timeout.d.ts +5 -0
- package/dist/runtime/timeout.js +12 -0
- package/dist/runtime/trace.d.ts +2 -2
- package/dist/runtime/trace.js +2 -4
- package/dist/runtime/waterfall.d.ts +1 -1
- package/dist/schema.d.ts +1 -1
- package/dist/schema.js +7 -15
- package/dist/serve.d.ts +1 -1
- package/dist/serve.js +1 -1
- package/dist/server/index.d.ts +7 -7
- package/dist/server/index.js +6 -6
- package/dist/server/self-test-input-tokens.d.ts +2 -1
- package/dist/server/self-test-input-tokens.js +18 -14
- package/dist/server/self-test-redaction.d.ts +1 -1
- package/dist/server/self-test-redaction.js +1 -1
- package/dist/server/self-test.d.ts +117 -3
- package/dist/server/self-test.js +787 -151
- package/dist/server/serve-implementation.d.ts +210 -0
- package/dist/server/serve-implementation.js +2078 -0
- package/dist/server/serve.d.ts +1 -70
- package/dist/server/serve.js +1 -1143
- package/dist/server/types.d.ts +34 -9
- package/dist/server/types.js +8 -1
- package/dist/stateful/errors.d.ts +19 -0
- package/dist/stateful/errors.js +24 -0
- package/dist/stateful/http-provider-event-emitter.d.ts +40 -0
- package/dist/stateful/http-provider-event-emitter.js +237 -0
- package/dist/stateful/http-session-owner-registry.d.ts +44 -0
- package/dist/stateful/http-session-owner-registry.js +210 -0
- package/dist/stateful/index.d.ts +18 -0
- package/dist/stateful/index.js +18 -0
- package/dist/stateful/provider-event-delivery-failures.d.ts +32 -0
- package/dist/stateful/provider-event-delivery-failures.js +43 -0
- package/dist/stateful/provider-event-pipeline-metrics.d.ts +46 -0
- package/dist/stateful/provider-event-pipeline-metrics.js +48 -0
- package/dist/stateful/provider-event-pipeline.d.ts +50 -0
- package/dist/stateful/provider-event-pipeline.js +1 -0
- package/dist/stateful/provider-events.d.ts +101 -0
- package/dist/stateful/provider-events.js +289 -0
- package/dist/stateful/session-key.d.ts +15 -0
- package/dist/stateful/session-key.js +86 -0
- package/dist/stateful/stateful-provider-adapter-context.d.ts +5 -0
- package/dist/stateful/stateful-provider-adapter-context.js +42 -0
- package/dist/stateful/stateful-provider-adapter-metrics.d.ts +15 -0
- package/dist/stateful/stateful-provider-adapter-metrics.js +21 -0
- package/dist/stateful/stateful-provider-adapter.d.ts +98 -0
- package/dist/stateful/stateful-provider-adapter.js +287 -0
- package/dist/stateful/stateful-provider-observability.d.ts +62 -0
- package/dist/stateful/stateful-provider-observability.js +161 -0
- package/dist/stateful/stateful-provider-owner-forwarder.d.ts +41 -0
- package/dist/stateful/stateful-provider-owner-forwarder.js +215 -0
- package/dist/stateful/stateful-provider-runtime-context.d.ts +32 -0
- package/dist/stateful/stateful-provider-runtime-context.js +60 -0
- package/dist/stateful/stateful-provider-runtime-executor.d.ts +34 -0
- package/dist/stateful/stateful-provider-runtime-executor.js +52 -0
- package/dist/stateful/stateful-provider-session-routing.d.ts +67 -0
- package/dist/stateful/stateful-provider-session-routing.js +345 -0
- package/dist/stateful/stateful-provider-session-runtime.d.ts +98 -0
- package/dist/stateful/stateful-provider-session-runtime.js +245 -0
- package/dist/stateful-signing.d.ts +18 -0
- package/dist/stateful-signing.js +27 -0
- package/dist/stealth/profiles.d.ts +1 -1
- package/dist/stealth/profiles.js +21 -21
- package/dist/stream-evidence.d.ts +74 -0
- package/dist/stream-evidence.js +785 -0
- package/dist/stream.d.ts +1 -1
- package/dist/stream.js +7 -1
- package/dist/testing/index.d.ts +3 -2
- package/dist/testing/index.js +3 -2
- package/dist/testing/run.d.ts +32 -2
- package/dist/testing/run.js +488 -28
- package/dist/types.d.ts +566 -19
- package/dist/types.js +1 -0
- package/dist/user-input.d.ts +30 -0
- package/dist/user-input.js +66 -0
- package/package.json +42 -7
- package/src/auth-turn/index.ts +2 -2
- package/src/auth.ts +146 -86
- package/src/ceremonies/index.ts +167 -92
- package/src/cli/commands.ts +10 -0
- package/src/cli/create.ts +42 -35
- package/src/cli/prompt-assets.ts +865 -0
- package/src/cli/templates/provider/AGENTS.md.tpl +17 -8
- package/src/cli/templates/provider/README.md.tpl +4 -4
- package/src/config/loader.ts +667 -289
- package/src/contract-serialization.ts +10 -18
- package/src/contract-types.ts +3 -2
- package/src/contract.ts +14 -28
- package/src/declaration-validation.ts +202 -0
- package/src/define.ts +631 -495
- package/src/dev.ts +4 -9
- package/src/error-resolution.ts +128 -0
- package/src/errors.ts +56 -11
- package/src/fixture-sanitization.ts +247 -0
- package/src/i18n/catalog.ts +10 -32
- package/src/i18n/index.ts +2 -2
- package/src/i18n/keys.ts +5 -11
- package/src/index.ts +158 -44
- package/src/lint.ts +488 -154
- package/src/native-address.ts +340 -0
- package/src/native-egress-policy.ts +358 -0
- package/src/observability.ts +51 -1
- package/src/provider.ts +66 -11
- package/src/public-schema-field-lint.ts +7 -33
- package/src/recipes/gov-api.ts +2 -5
- package/src/runtime/auth-flow.ts +13 -7
- package/src/runtime/browser.ts +252 -207
- package/src/runtime/cache.ts +209 -81
- package/src/runtime/choice-wordlist.ts +145 -0
- package/src/runtime/choice.ts +758 -197
- package/src/runtime/credential.ts +2 -2
- package/src/runtime/env.ts +1 -1
- package/src/runtime/executor.ts +37 -19
- package/src/runtime/http.ts +645 -65
- package/src/runtime/insights.ts +15 -53
- package/src/runtime/instrumentation.ts +530 -67
- package/src/runtime/keyring.ts +7 -19
- package/src/runtime/namespace.ts +2 -7
- package/src/runtime/native-network-errors.ts +99 -0
- package/src/runtime/native-network.ts +1605 -0
- package/src/runtime/ocr.ts +523 -0
- package/src/runtime/otlp.ts +12 -23
- package/src/runtime/perf.ts +1 -1
- package/src/runtime/provider.ts +4 -9
- package/src/runtime/proxy-errors.ts +29 -42
- package/src/runtime/proxy-nodemaven.ts +221 -0
- package/src/runtime/proxy-retry-policy.ts +3 -3
- package/src/runtime/proxy-telemetry.ts +84 -77
- package/src/runtime/redirects.ts +66 -0
- package/src/runtime/redis.ts +10 -13
- package/src/runtime/request-options.ts +679 -9
- package/src/runtime/resolver-config.ts +6 -0
- package/src/runtime/resolver-public.ts +18 -0
- package/src/runtime/resolver-shared.ts +17 -0
- package/src/runtime/resolver-vendors/bindings.ts +56 -0
- package/src/runtime/resolver-vendors/browser.ts +408 -0
- package/src/runtime/resolver-vendors/hosts.ts +38 -0
- package/src/runtime/resolver-vendors/twocaptcha.ts +500 -0
- package/src/runtime/resolver-vendors/types.ts +173 -0
- package/src/runtime/resolver.ts +1060 -0
- package/src/runtime/secrets.ts +64 -0
- package/src/runtime/state.ts +399 -161
- package/src/runtime/stealth-cookies.ts +132 -0
- package/src/runtime/stealth.ts +681 -295
- package/src/runtime/stt.ts +39 -113
- package/src/runtime/timeout.ts +18 -0
- package/src/runtime/trace.ts +14 -44
- package/src/runtime/waterfall.ts +5 -18
- package/src/schema.ts +23 -84
- package/src/serve.ts +6 -1
- package/src/server/index.ts +30 -7
- package/src/server/self-test-input-tokens.ts +29 -14
- package/src/server/self-test-redaction.ts +2 -2
- package/src/server/self-test.ts +1030 -180
- package/src/server/serve-implementation.ts +3062 -0
- package/src/server/serve.ts +1 -1781
- package/src/server/types.ts +12 -13
- package/src/stateful/README.md +146 -0
- package/src/stateful/errors.ts +35 -0
- package/src/stateful/http-provider-event-emitter.ts +314 -0
- package/src/stateful/http-session-owner-registry.ts +306 -0
- package/src/stateful/index.ts +18 -0
- package/src/stateful/provider-event-delivery-failures.ts +80 -0
- package/src/stateful/provider-event-pipeline-metrics.ts +95 -0
- package/src/stateful/provider-event-pipeline.ts +61 -0
- package/src/stateful/provider-events.ts +462 -0
- package/src/stateful/session-key.ts +111 -0
- package/src/stateful/stateful-provider-adapter-context.ts +59 -0
- package/src/stateful/stateful-provider-adapter-metrics.ts +48 -0
- package/src/stateful/stateful-provider-adapter.ts +562 -0
- package/src/stateful/stateful-provider-observability.ts +261 -0
- package/src/stateful/stateful-provider-owner-forwarder.ts +287 -0
- package/src/stateful/stateful-provider-runtime-context.ts +92 -0
- package/src/stateful/stateful-provider-runtime-executor.ts +96 -0
- package/src/stateful/stateful-provider-session-routing.ts +546 -0
- package/src/stateful/stateful-provider-session-runtime.ts +403 -0
- package/src/stateful-signing.ts +46 -0
- package/src/stealth/profiles.ts +27 -33
- package/src/stream-evidence.ts +988 -0
- package/src/stream.ts +16 -20
- package/src/testing/index.ts +11 -2
- package/src/testing/run.ts +668 -74
- package/src/types.ts +665 -35
- package/src/user-input.ts +118 -0
- package/dist/cli/templates/provider/CLAUDE.md.tpl +0 -1
- package/src/cli/templates/provider/CLAUDE.md.tpl +0 -1
- /package/dist/cli/templates/provider/{skills → .agents/skills}/fixtures-and-recording/SKILL.md.tpl +0 -0
- /package/dist/cli/templates/provider/{skills → .agents/skills}/health-checks-and-fail-closed/SKILL.md.tpl +0 -0
- /package/dist/cli/templates/provider/{skills → .agents/skills}/normalization-standards/SKILL.md.tpl +0 -0
- /package/dist/cli/templates/provider/{skills → .agents/skills}/pagination-and-counts/SKILL.md.tpl +0 -0
- /package/dist/cli/templates/provider/{skills → .agents/skills}/upstream-contract-verification/SKILL.md.tpl +0 -0
- /package/dist/cli/templates/provider/{skills → .agents/skills}/upstream-notes/README.md.tpl +0 -0
- /package/src/cli/templates/provider/{skills → .agents/skills}/fixtures-and-recording/SKILL.md.tpl +0 -0
- /package/src/cli/templates/provider/{skills → .agents/skills}/health-checks-and-fail-closed/SKILL.md.tpl +0 -0
- /package/src/cli/templates/provider/{skills → .agents/skills}/normalization-standards/SKILL.md.tpl +0 -0
- /package/src/cli/templates/provider/{skills → .agents/skills}/pagination-and-counts/SKILL.md.tpl +0 -0
- /package/src/cli/templates/provider/{skills → .agents/skills}/upstream-contract-verification/SKILL.md.tpl +0 -0
- /package/src/cli/templates/provider/{skills → .agents/skills}/upstream-notes/README.md.tpl +0 -0
package/dist/runtime/choice.js
CHANGED
|
@@ -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
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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
|
-
|
|
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
|
|
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:
|
|
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
|
|
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
|
-
|
|
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) {
|