@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.
- package/AUTHORING.md +493 -5
- package/CHANGELOG.md +127 -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 +122 -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 +13 -1
- package/dist/runtime/choice.js +514 -124
- 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 +2077 -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 +127 -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 +157 -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 +65 -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 +685 -216
- 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 +3060 -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,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
|
|
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,
|
|
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
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
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
|
|
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
|
-
|
|
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) {
|