@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/src/runtime/choice.ts
CHANGED
|
@@ -4,32 +4,47 @@ import {
|
|
|
4
4
|
createHash,
|
|
5
5
|
createHmac,
|
|
6
6
|
randomBytes,
|
|
7
|
+
randomInt,
|
|
7
8
|
timingSafeEqual,
|
|
8
9
|
} from "node:crypto";
|
|
9
10
|
import {
|
|
10
11
|
assertFreshProviderChoiceIssuedAt,
|
|
11
12
|
ProviderChoiceTokenError,
|
|
12
13
|
type ProviderChoiceTokenPayload,
|
|
13
|
-
} from "../choice-token";
|
|
14
|
-
import { ProviderError } from "../errors";
|
|
14
|
+
} from "../choice-token.js";
|
|
15
|
+
import { isProviderError, ProviderError } from "../errors.js";
|
|
16
|
+
import {
|
|
17
|
+
CHOICE_WORDLIST_SIZE,
|
|
18
|
+
choiceWordAt,
|
|
19
|
+
HIGH_CHOICE_WORD_COUNT,
|
|
20
|
+
isChoiceWord,
|
|
21
|
+
STANDARD_CHOICE_WORD_COUNT,
|
|
22
|
+
} from "./choice-wordlist.js";
|
|
15
23
|
import type {
|
|
16
24
|
CredentialContext,
|
|
17
25
|
EnvContext,
|
|
18
26
|
ProviderChoiceBindingOptions,
|
|
27
|
+
ProviderChoiceConsumeMode,
|
|
28
|
+
ProviderChoiceConsumeResult,
|
|
19
29
|
ProviderChoiceContext,
|
|
30
|
+
ProviderChoiceExplicitParseResult,
|
|
20
31
|
ProviderChoiceIssueOptions,
|
|
21
32
|
ProviderChoiceParseOptions,
|
|
22
33
|
ProviderChoiceStorageOptions,
|
|
23
34
|
ProviderRequestContext,
|
|
24
35
|
ProviderRuntimeState,
|
|
25
36
|
ProviderStateDurationString,
|
|
26
|
-
|
|
37
|
+
StateValue,
|
|
38
|
+
} from "../types.js";
|
|
27
39
|
|
|
28
40
|
export const PROVIDER_RUNTIME_CHOICE_TOKEN_MASTER_SECRET_ENV =
|
|
29
41
|
"APIFUSE__PROVIDER_RUNTIME__CHOICE_TOKEN_MASTER_SECRET";
|
|
30
42
|
|
|
31
43
|
const PRIMARY_CHOICE_TOKEN_KID = "v1";
|
|
32
44
|
const MANAGED_CHOICE_TOKEN_VERSION = 1;
|
|
45
|
+
const SERVER_STORED_CHOICE_RECORD_VERSION = 1;
|
|
46
|
+
const SERVER_STORED_CHOICE_ISSUE_ATTEMPTS = 5;
|
|
47
|
+
const WORD_CHOICE_NOT_FOUND_MESSAGE = "Provider choice token was not found.";
|
|
33
48
|
|
|
34
49
|
type ManagedChoiceEnvelope = {
|
|
35
50
|
readonly v: typeof MANAGED_CHOICE_TOKEN_VERSION;
|
|
@@ -51,6 +66,32 @@ type ServerChoiceHandlePayload = {
|
|
|
51
66
|
readonly created_at_ms: number;
|
|
52
67
|
};
|
|
53
68
|
|
|
69
|
+
type ServerStoredChoiceRecord = {
|
|
70
|
+
readonly v: typeof SERVER_STORED_CHOICE_RECORD_VERSION;
|
|
71
|
+
readonly storage: "server";
|
|
72
|
+
readonly status: "active" | "consumed";
|
|
73
|
+
readonly provider_id: string;
|
|
74
|
+
readonly purpose: string;
|
|
75
|
+
readonly issued_at_ms: number;
|
|
76
|
+
readonly ttl_ms: number;
|
|
77
|
+
readonly binding?: ManagedChoiceEnvelope["binding"];
|
|
78
|
+
readonly prefix: string;
|
|
79
|
+
readonly payload: ProviderChoiceTokenPayload;
|
|
80
|
+
readonly payload_digest: string;
|
|
81
|
+
readonly replay_key: string;
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
export type ProviderChoiceTelemetryEvent = {
|
|
85
|
+
readonly providerId: string;
|
|
86
|
+
readonly purpose: string;
|
|
87
|
+
readonly operation: "parse" | "consume";
|
|
88
|
+
readonly format: "word" | "legacy";
|
|
89
|
+
readonly outcome: "success" | "not-found" | "invalid" | "unsupported" | "error";
|
|
90
|
+
readonly consumeMode: ProviderChoiceConsumeMode;
|
|
91
|
+
readonly consumed: boolean;
|
|
92
|
+
readonly replay: boolean;
|
|
93
|
+
};
|
|
94
|
+
|
|
54
95
|
export type CreateProviderChoiceContextOptions = {
|
|
55
96
|
readonly providerId: string;
|
|
56
97
|
readonly env?: EnvContext;
|
|
@@ -59,6 +100,8 @@ export type CreateProviderChoiceContextOptions = {
|
|
|
59
100
|
readonly state?: ProviderRuntimeState;
|
|
60
101
|
readonly masterSecret?: string;
|
|
61
102
|
readonly kid?: string;
|
|
103
|
+
/** Receives allowlisted metadata only; token and payload values are never included. */
|
|
104
|
+
readonly onTelemetry?: (event: ProviderChoiceTelemetryEvent) => void;
|
|
62
105
|
};
|
|
63
106
|
|
|
64
107
|
export function createProviderChoiceContext(
|
|
@@ -74,24 +117,48 @@ export function createProviderChoiceContext(
|
|
|
74
117
|
): string;
|
|
75
118
|
function issue<TPayload extends ProviderChoiceTokenPayload>(
|
|
76
119
|
issueOptions: ProviderChoiceIssueOptions<TPayload> & {
|
|
77
|
-
readonly storage: Extract<
|
|
78
|
-
ProviderChoiceStorageOptions,
|
|
79
|
-
{ readonly mode: "server" }
|
|
80
|
-
>;
|
|
120
|
+
readonly storage: Extract<ProviderChoiceStorageOptions, { readonly mode: "server" }>;
|
|
81
121
|
},
|
|
82
122
|
): Promise<string>;
|
|
83
123
|
function issue<TPayload extends ProviderChoiceTokenPayload>(
|
|
84
124
|
issueOptions: ProviderChoiceIssueOptions<TPayload> & {
|
|
85
|
-
readonly storage: Extract<
|
|
86
|
-
ProviderChoiceStorageOptions,
|
|
87
|
-
{ readonly mode: "auto" }
|
|
88
|
-
>;
|
|
125
|
+
readonly storage: Extract<ProviderChoiceStorageOptions, { readonly mode: "auto" }>;
|
|
89
126
|
},
|
|
90
127
|
): string | Promise<string>;
|
|
91
128
|
function issue<TPayload extends ProviderChoiceTokenPayload>(
|
|
92
129
|
issueOptions: ProviderChoiceIssueOptions<TPayload>,
|
|
93
130
|
): string | Promise<string> {
|
|
94
131
|
const issuedAtMs = issueOptions.nowMs ?? Date.now();
|
|
132
|
+
const resolvedStorage = resolveIssueStorage(issueOptions.storage, issueOptions.payload);
|
|
133
|
+
if (resolvedStorage.mode === "server") {
|
|
134
|
+
const binding = hasRequestedChoiceBinding(issueOptions.bind)
|
|
135
|
+
? createChoiceBinding({
|
|
136
|
+
keys: deriveManagedChoiceKeys({
|
|
137
|
+
masterSecret: resolveMasterSecret(),
|
|
138
|
+
providerId: options.providerId,
|
|
139
|
+
purpose: issueOptions.purpose,
|
|
140
|
+
kid,
|
|
141
|
+
}),
|
|
142
|
+
options: issueOptions.bind,
|
|
143
|
+
request: options.request,
|
|
144
|
+
credential: options.credential,
|
|
145
|
+
required: true,
|
|
146
|
+
})
|
|
147
|
+
: undefined;
|
|
148
|
+
return issueServerStoredChoice({
|
|
149
|
+
baseEnvelope: {
|
|
150
|
+
v: MANAGED_CHOICE_TOKEN_VERSION,
|
|
151
|
+
provider_id: options.providerId,
|
|
152
|
+
purpose: issueOptions.purpose,
|
|
153
|
+
issued_at_ms: issuedAtMs,
|
|
154
|
+
ttl_ms: issueOptions.ttlMs,
|
|
155
|
+
binding,
|
|
156
|
+
},
|
|
157
|
+
issueOptions,
|
|
158
|
+
storage: resolvedStorage.storage,
|
|
159
|
+
contextState: options.state,
|
|
160
|
+
});
|
|
161
|
+
}
|
|
95
162
|
const keys = deriveManagedChoiceKeys({
|
|
96
163
|
masterSecret: resolveMasterSecret(),
|
|
97
164
|
providerId: options.providerId,
|
|
@@ -112,21 +179,6 @@ export function createProviderChoiceContext(
|
|
|
112
179
|
required: true,
|
|
113
180
|
}),
|
|
114
181
|
};
|
|
115
|
-
const resolvedStorage = resolveIssueStorage(
|
|
116
|
-
issueOptions.storage,
|
|
117
|
-
issueOptions.payload,
|
|
118
|
-
);
|
|
119
|
-
if (resolvedStorage.mode === "server") {
|
|
120
|
-
return issueServerStoredChoice({
|
|
121
|
-
baseEnvelope,
|
|
122
|
-
issueOptions,
|
|
123
|
-
storage: resolvedStorage.storage,
|
|
124
|
-
contextState: options.state,
|
|
125
|
-
kid,
|
|
126
|
-
keys,
|
|
127
|
-
issuedAtMs,
|
|
128
|
-
});
|
|
129
|
-
}
|
|
130
182
|
const envelope: ManagedChoiceEnvelope = {
|
|
131
183
|
...baseEnvelope,
|
|
132
184
|
payload: issueOptions.payload,
|
|
@@ -139,6 +191,9 @@ export function createProviderChoiceContext(
|
|
|
139
191
|
});
|
|
140
192
|
}
|
|
141
193
|
|
|
194
|
+
function parse(
|
|
195
|
+
parseOptions: ProviderChoiceParseOptions & { readonly consume: "explicit" },
|
|
196
|
+
): Promise<ProviderChoiceExplicitParseResult>;
|
|
142
197
|
function parse(
|
|
143
198
|
parseOptions: ProviderChoiceParseOptions & {
|
|
144
199
|
readonly storage?: { readonly mode: "inline" };
|
|
@@ -146,94 +201,170 @@ export function createProviderChoiceContext(
|
|
|
146
201
|
): ProviderChoiceTokenPayload;
|
|
147
202
|
function parse(
|
|
148
203
|
parseOptions: ProviderChoiceParseOptions & {
|
|
149
|
-
readonly storage: Extract<
|
|
150
|
-
ProviderChoiceStorageOptions,
|
|
151
|
-
{ readonly mode: "server" }
|
|
152
|
-
>;
|
|
204
|
+
readonly storage: Extract<ProviderChoiceStorageOptions, { readonly mode: "server" }>;
|
|
153
205
|
},
|
|
154
206
|
): Promise<ProviderChoiceTokenPayload>;
|
|
155
207
|
function parse(
|
|
156
208
|
parseOptions: ProviderChoiceParseOptions & {
|
|
157
|
-
readonly storage: Extract<
|
|
158
|
-
ProviderChoiceStorageOptions,
|
|
159
|
-
{ readonly mode: "auto" }
|
|
160
|
-
>;
|
|
209
|
+
readonly storage: Extract<ProviderChoiceStorageOptions, { readonly mode: "auto" }>;
|
|
161
210
|
},
|
|
162
211
|
): ProviderChoiceTokenPayload | Promise<ProviderChoiceTokenPayload>;
|
|
163
212
|
function parse(
|
|
164
213
|
parseOptions: ProviderChoiceParseOptions,
|
|
165
|
-
):
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
] = parseManagedChoiceTokenParts(parseOptions.token);
|
|
174
|
-
if (
|
|
175
|
-
actualPrefix !== parseOptions.prefix ||
|
|
176
|
-
tokenKid !== kid ||
|
|
177
|
-
!encodedIv ||
|
|
178
|
-
!encryptedPayload ||
|
|
179
|
-
!authTag ||
|
|
180
|
-
!signature
|
|
181
|
-
) {
|
|
182
|
-
throw new ProviderChoiceTokenError(
|
|
183
|
-
"invalid_shape",
|
|
184
|
-
"Provider choice token shape is invalid.",
|
|
185
|
-
);
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
const keys = deriveManagedChoiceKeys({
|
|
189
|
-
masterSecret: resolveMasterSecret(),
|
|
190
|
-
providerId: options.providerId,
|
|
191
|
-
purpose: parseOptions.purpose,
|
|
192
|
-
kid: tokenKid,
|
|
193
|
-
});
|
|
194
|
-
const signedBody = [
|
|
195
|
-
parseOptions.prefix,
|
|
196
|
-
tokenKid,
|
|
197
|
-
encodedIv,
|
|
198
|
-
encryptedPayload,
|
|
199
|
-
authTag,
|
|
200
|
-
].join(".");
|
|
201
|
-
assertManagedChoiceSignature({
|
|
202
|
-
signedBody,
|
|
203
|
-
signature,
|
|
204
|
-
signingKey: keys.signing,
|
|
205
|
-
});
|
|
206
|
-
const envelope = decryptManagedChoiceToken({
|
|
207
|
-
encodedIv,
|
|
208
|
-
encryptedPayload,
|
|
209
|
-
authTag,
|
|
210
|
-
encryptionKey: keys.encryption,
|
|
211
|
-
});
|
|
212
|
-
assertManagedChoiceEnvelope(envelope, {
|
|
213
|
-
providerId: options.providerId,
|
|
214
|
-
purpose: parseOptions.purpose,
|
|
215
|
-
ttlMs: parseOptions.ttlMs,
|
|
216
|
-
nowMs: parseOptions.nowMs,
|
|
217
|
-
futureToleranceMs: parseOptions.futureToleranceMs,
|
|
214
|
+
):
|
|
215
|
+
| ProviderChoiceTokenPayload
|
|
216
|
+
| ProviderChoiceExplicitParseResult
|
|
217
|
+
| Promise<ProviderChoiceTokenPayload | ProviderChoiceExplicitParseResult> {
|
|
218
|
+
const consumeMode = parseOptions.consume ?? "never";
|
|
219
|
+
const wordStateKey = parseWordChoiceStateKey({
|
|
220
|
+
token: parseOptions.token,
|
|
221
|
+
prefix: parseOptions.prefix,
|
|
218
222
|
});
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
223
|
+
if (wordStateKey) {
|
|
224
|
+
const parsed = parseWordServerStoredChoice({
|
|
225
|
+
stateKey: wordStateKey,
|
|
226
|
+
parseOptions,
|
|
227
|
+
contextState: options.state,
|
|
228
|
+
providerId: options.providerId,
|
|
224
229
|
request: options.request,
|
|
225
230
|
credential: options.credential,
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
231
|
+
resolveBindingKeys: () =>
|
|
232
|
+
deriveManagedChoiceKeys({
|
|
233
|
+
masterSecret: resolveMasterSecret(),
|
|
234
|
+
providerId: options.providerId,
|
|
235
|
+
purpose: parseOptions.purpose,
|
|
236
|
+
kid,
|
|
237
|
+
}),
|
|
238
|
+
onConsume: (result) =>
|
|
239
|
+
emitChoiceTelemetry(options.onTelemetry, {
|
|
240
|
+
providerId: options.providerId,
|
|
241
|
+
purpose: parseOptions.purpose,
|
|
242
|
+
operation: "consume",
|
|
243
|
+
format: "word",
|
|
244
|
+
outcome: "success",
|
|
245
|
+
consumeMode,
|
|
246
|
+
consumed: result.status === "consumed",
|
|
247
|
+
replay: result.status === "already-consumed",
|
|
248
|
+
}),
|
|
249
|
+
});
|
|
250
|
+
return observeChoiceParse<
|
|
251
|
+
ProviderChoiceTokenPayload | ProviderChoiceExplicitParseResult
|
|
252
|
+
>(parsed, {
|
|
253
|
+
onTelemetry: options.onTelemetry,
|
|
254
|
+
providerId: options.providerId,
|
|
255
|
+
purpose: parseOptions.purpose,
|
|
256
|
+
format: "word",
|
|
257
|
+
consumeMode,
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Legacy encrypted-envelope compatibility fallback. Removal is gated on
|
|
262
|
+
// the last legacy mint plus the maximum issued TTL; see ADR 0006.
|
|
263
|
+
// A structurally valid word token returns above, so lookup, expiry,
|
|
264
|
+
// consumption, and binding failures can never enter this branch.
|
|
265
|
+
try {
|
|
266
|
+
const [actualPrefix, tokenKid, encodedIv, encryptedPayload, authTag, signature] =
|
|
267
|
+
parseManagedChoiceTokenParts(parseOptions.token);
|
|
268
|
+
if (
|
|
269
|
+
actualPrefix !== parseOptions.prefix ||
|
|
270
|
+
tokenKid !== kid ||
|
|
271
|
+
!encodedIv ||
|
|
272
|
+
!encryptedPayload ||
|
|
273
|
+
!authTag ||
|
|
274
|
+
!signature
|
|
275
|
+
) {
|
|
276
|
+
throw new ProviderChoiceTokenError(
|
|
277
|
+
"invalid_shape",
|
|
278
|
+
"Provider choice token shape is invalid.",
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const keys = deriveManagedChoiceKeys({
|
|
283
|
+
masterSecret: resolveMasterSecret(),
|
|
284
|
+
providerId: options.providerId,
|
|
285
|
+
purpose: parseOptions.purpose,
|
|
286
|
+
kid: tokenKid,
|
|
287
|
+
});
|
|
288
|
+
const signedBody = [
|
|
289
|
+
parseOptions.prefix,
|
|
290
|
+
tokenKid,
|
|
291
|
+
encodedIv,
|
|
292
|
+
encryptedPayload,
|
|
293
|
+
authTag,
|
|
294
|
+
].join(".");
|
|
295
|
+
assertManagedChoiceSignature({
|
|
296
|
+
signedBody,
|
|
297
|
+
signature,
|
|
298
|
+
signingKey: keys.signing,
|
|
299
|
+
});
|
|
300
|
+
const envelope = decryptManagedChoiceToken({
|
|
301
|
+
encodedIv,
|
|
302
|
+
encryptedPayload,
|
|
303
|
+
authTag,
|
|
304
|
+
encryptionKey: keys.encryption,
|
|
305
|
+
});
|
|
306
|
+
assertManagedChoiceEnvelope(envelope, {
|
|
307
|
+
providerId: options.providerId,
|
|
308
|
+
purpose: parseOptions.purpose,
|
|
309
|
+
ttlMs: parseOptions.ttlMs,
|
|
310
|
+
nowMs: parseOptions.nowMs,
|
|
311
|
+
futureToleranceMs: parseOptions.futureToleranceMs,
|
|
312
|
+
});
|
|
313
|
+
assertChoiceBindingMatches({
|
|
314
|
+
actual: envelope.binding,
|
|
315
|
+
expected: createChoiceBinding({
|
|
316
|
+
keys,
|
|
317
|
+
options: parseOptions.bind,
|
|
318
|
+
request: options.request,
|
|
319
|
+
credential: options.credential,
|
|
320
|
+
required: true,
|
|
321
|
+
}),
|
|
234
322
|
});
|
|
323
|
+
const payload = isServerChoiceHandlePayload(envelope.payload)
|
|
324
|
+
? parseLegacyServerStoredChoice({
|
|
325
|
+
handle: envelope.payload,
|
|
326
|
+
storage: parseOptions.storage,
|
|
327
|
+
contextState: options.state,
|
|
328
|
+
})
|
|
329
|
+
: envelope.payload;
|
|
330
|
+
const parsed =
|
|
331
|
+
consumeMode === "explicit"
|
|
332
|
+
? Promise.resolve(payload).then((resolvedPayload) =>
|
|
333
|
+
createLegacyExplicitParseResult({
|
|
334
|
+
payload: resolvedPayload,
|
|
335
|
+
replayKey: digestChoiceReplayKey(parseOptions.token),
|
|
336
|
+
onConsume: () =>
|
|
337
|
+
emitChoiceTelemetry(options.onTelemetry, {
|
|
338
|
+
providerId: options.providerId,
|
|
339
|
+
purpose: parseOptions.purpose,
|
|
340
|
+
operation: "consume",
|
|
341
|
+
format: "legacy",
|
|
342
|
+
outcome: "unsupported",
|
|
343
|
+
consumeMode,
|
|
344
|
+
consumed: false,
|
|
345
|
+
replay: false,
|
|
346
|
+
}),
|
|
347
|
+
}),
|
|
348
|
+
)
|
|
349
|
+
: payload;
|
|
350
|
+
return observeChoiceParse<
|
|
351
|
+
ProviderChoiceTokenPayload | ProviderChoiceExplicitParseResult
|
|
352
|
+
>(parsed, {
|
|
353
|
+
onTelemetry: options.onTelemetry,
|
|
354
|
+
providerId: options.providerId,
|
|
355
|
+
purpose: parseOptions.purpose,
|
|
356
|
+
format: "legacy",
|
|
357
|
+
consumeMode,
|
|
358
|
+
});
|
|
359
|
+
} catch (error) {
|
|
360
|
+
emitChoiceParseFailure(options.onTelemetry, error, {
|
|
361
|
+
providerId: options.providerId,
|
|
362
|
+
purpose: parseOptions.purpose,
|
|
363
|
+
format: "legacy",
|
|
364
|
+
consumeMode,
|
|
365
|
+
});
|
|
366
|
+
throw error;
|
|
235
367
|
}
|
|
236
|
-
return envelope.payload;
|
|
237
368
|
}
|
|
238
369
|
|
|
239
370
|
return { issue, parse };
|
|
@@ -247,30 +378,126 @@ export function createTestProviderChoiceContext(
|
|
|
247
378
|
return createProviderChoiceContext({
|
|
248
379
|
...options,
|
|
249
380
|
masterSecret:
|
|
250
|
-
options.masterSecret ??
|
|
251
|
-
"apifuse-test-provider-runtime-choice-token-master-secret",
|
|
381
|
+
options.masterSecret ?? "apifuse-test-provider-runtime-choice-token-master-secret",
|
|
252
382
|
});
|
|
253
383
|
}
|
|
254
384
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
385
|
+
type ChoiceParseTelemetryBase = {
|
|
386
|
+
readonly onTelemetry?: (event: ProviderChoiceTelemetryEvent) => void;
|
|
387
|
+
readonly providerId: string;
|
|
388
|
+
readonly purpose: string;
|
|
389
|
+
readonly format: "word" | "legacy";
|
|
390
|
+
readonly consumeMode: ProviderChoiceConsumeMode;
|
|
391
|
+
};
|
|
392
|
+
|
|
393
|
+
function observeChoiceParse<T>(
|
|
394
|
+
result: T | Promise<T>,
|
|
395
|
+
base: ChoiceParseTelemetryBase,
|
|
396
|
+
): T | Promise<T> {
|
|
397
|
+
if (result instanceof Promise) {
|
|
398
|
+
return result.then(
|
|
399
|
+
(value) => {
|
|
400
|
+
emitChoiceParseSuccess(base, value);
|
|
401
|
+
return value;
|
|
402
|
+
},
|
|
403
|
+
(error: unknown) => {
|
|
404
|
+
emitChoiceParseFailure(base.onTelemetry, error, base);
|
|
405
|
+
throw error;
|
|
406
|
+
},
|
|
407
|
+
);
|
|
408
|
+
}
|
|
409
|
+
emitChoiceParseSuccess(base, result);
|
|
410
|
+
return result;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function emitChoiceParseSuccess(base: ChoiceParseTelemetryBase, result: unknown): void {
|
|
414
|
+
const replay = isConsumedChoiceReplay(result);
|
|
415
|
+
emitChoiceTelemetry(base.onTelemetry, {
|
|
416
|
+
providerId: base.providerId,
|
|
417
|
+
purpose: base.purpose,
|
|
418
|
+
operation: "parse",
|
|
419
|
+
format: base.format,
|
|
420
|
+
outcome: "success",
|
|
421
|
+
consumeMode: base.consumeMode,
|
|
422
|
+
consumed: replay || (base.format === "word" && base.consumeMode === "on-parse"),
|
|
423
|
+
replay,
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function isConsumedChoiceReplay(value: unknown): boolean {
|
|
428
|
+
return (
|
|
429
|
+
value !== null &&
|
|
430
|
+
typeof value === "object" &&
|
|
431
|
+
"status" in value &&
|
|
432
|
+
value.status === "consumed" &&
|
|
433
|
+
"replayKey" in value &&
|
|
434
|
+
typeof value.replayKey === "string"
|
|
435
|
+
);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function emitChoiceParseFailure(
|
|
439
|
+
onTelemetry: CreateProviderChoiceContextOptions["onTelemetry"],
|
|
440
|
+
error: unknown,
|
|
441
|
+
base: Omit<ChoiceParseTelemetryBase, "onTelemetry">,
|
|
442
|
+
): void {
|
|
443
|
+
const outcome =
|
|
444
|
+
error instanceof ProviderChoiceTokenError
|
|
445
|
+
? base.format === "word" && error.message === WORD_CHOICE_NOT_FOUND_MESSAGE
|
|
446
|
+
? "not-found"
|
|
447
|
+
: "invalid"
|
|
448
|
+
: "error";
|
|
449
|
+
emitChoiceTelemetry(onTelemetry, {
|
|
450
|
+
providerId: base.providerId,
|
|
451
|
+
purpose: base.purpose,
|
|
452
|
+
operation: "parse",
|
|
453
|
+
format: base.format,
|
|
454
|
+
outcome,
|
|
455
|
+
consumeMode: base.consumeMode,
|
|
456
|
+
consumed: false,
|
|
457
|
+
replay: false,
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function emitChoiceTelemetry(
|
|
462
|
+
onTelemetry: CreateProviderChoiceContextOptions["onTelemetry"],
|
|
463
|
+
event: ProviderChoiceTelemetryEvent,
|
|
464
|
+
): void {
|
|
465
|
+
try {
|
|
466
|
+
onTelemetry?.(event);
|
|
467
|
+
} catch {
|
|
468
|
+
// Observability must never change provider token semantics.
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
function createLegacyExplicitParseResult(options: {
|
|
473
|
+
readonly payload: ProviderChoiceTokenPayload;
|
|
474
|
+
readonly replayKey: string;
|
|
475
|
+
readonly onConsume: () => void;
|
|
476
|
+
}): ProviderChoiceExplicitParseResult {
|
|
477
|
+
return {
|
|
478
|
+
status: "active",
|
|
479
|
+
payload: options.payload,
|
|
480
|
+
replayKey: options.replayKey,
|
|
481
|
+
consume: async () => {
|
|
482
|
+
options.onConsume();
|
|
483
|
+
return { status: "unsupported" };
|
|
484
|
+
},
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
function resolveChoiceMasterSecret(options: CreateProviderChoiceContextOptions): string {
|
|
258
489
|
const configured =
|
|
259
|
-
options.masterSecret ??
|
|
260
|
-
options.env?.get(PROVIDER_RUNTIME_CHOICE_TOKEN_MASTER_SECRET_ENV);
|
|
490
|
+
options.masterSecret ?? options.env?.get(PROVIDER_RUNTIME_CHOICE_TOKEN_MASTER_SECRET_ENV);
|
|
261
491
|
const trimmed = configured?.trim();
|
|
262
492
|
if (trimmed) return trimmed;
|
|
263
|
-
throw new ProviderError(
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
details: {
|
|
270
|
-
secret: PROVIDER_RUNTIME_CHOICE_TOKEN_MASTER_SECRET_ENV,
|
|
271
|
-
},
|
|
493
|
+
throw new ProviderError("Provider runtime choice-token master secret is not configured.", {
|
|
494
|
+
code: "CHOICE_TOKEN_MASTER_SECRET_NOT_CONFIGURED",
|
|
495
|
+
category: "internal_error",
|
|
496
|
+
retryable: false,
|
|
497
|
+
details: {
|
|
498
|
+
secret: PROVIDER_RUNTIME_CHOICE_TOKEN_MASTER_SECRET_ENV,
|
|
272
499
|
},
|
|
273
|
-
);
|
|
500
|
+
});
|
|
274
501
|
}
|
|
275
502
|
|
|
276
503
|
type ManagedChoiceKeyInput = {
|
|
@@ -286,9 +513,7 @@ type ManagedChoiceKeys = {
|
|
|
286
513
|
readonly binding: Buffer;
|
|
287
514
|
};
|
|
288
515
|
|
|
289
|
-
function deriveManagedChoiceKeys(
|
|
290
|
-
input: ManagedChoiceKeyInput,
|
|
291
|
-
): ManagedChoiceKeys {
|
|
516
|
+
function deriveManagedChoiceKeys(input: ManagedChoiceKeyInput): ManagedChoiceKeys {
|
|
292
517
|
return {
|
|
293
518
|
encryption: deriveManagedChoiceKey(input, "encryption"),
|
|
294
519
|
signing: deriveManagedChoiceKey(input, "signing"),
|
|
@@ -327,76 +552,212 @@ function encryptManagedChoiceToken(options: {
|
|
|
327
552
|
]).toString("base64url");
|
|
328
553
|
const authTag = cipher.getAuthTag().toString("base64url");
|
|
329
554
|
const encodedIv = iv.toString("base64url");
|
|
330
|
-
const signedBody = [
|
|
331
|
-
options.prefix,
|
|
332
|
-
options.kid,
|
|
333
|
-
encodedIv,
|
|
334
|
-
encryptedPayload,
|
|
335
|
-
authTag,
|
|
336
|
-
].join(".");
|
|
555
|
+
const signedBody = [options.prefix, options.kid, encodedIv, encryptedPayload, authTag].join(".");
|
|
337
556
|
const signature = createHmac("sha256", options.keys.signing)
|
|
338
557
|
.update(signedBody)
|
|
339
558
|
.digest("base64url");
|
|
340
559
|
return `${signedBody}.${signature}`;
|
|
341
560
|
}
|
|
342
561
|
|
|
343
|
-
async function issueServerStoredChoice<
|
|
344
|
-
TPayload extends ProviderChoiceTokenPayload,
|
|
345
|
-
>(options: {
|
|
562
|
+
async function issueServerStoredChoice<TPayload extends ProviderChoiceTokenPayload>(options: {
|
|
346
563
|
readonly baseEnvelope: Omit<ManagedChoiceEnvelope, "payload">;
|
|
347
564
|
readonly issueOptions: ProviderChoiceIssueOptions<TPayload>;
|
|
348
565
|
readonly storage: ServerProviderChoiceStorageOptions;
|
|
349
566
|
readonly contextState?: ProviderRuntimeState;
|
|
350
|
-
readonly kid: string;
|
|
351
|
-
readonly keys: ManagedChoiceKeys;
|
|
352
|
-
readonly issuedAtMs: number;
|
|
353
567
|
}): Promise<string> {
|
|
354
|
-
const serializedPayload = serializeChoicePayload(
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
568
|
+
const serializedPayload = serializeChoicePayload(options.issueOptions.payload);
|
|
569
|
+
const payloadDigest = digestChoicePayload(serializedPayload);
|
|
570
|
+
const namespace = resolveChoiceStateNamespace({
|
|
571
|
+
storage: options.storage,
|
|
572
|
+
contextState: options.contextState,
|
|
573
|
+
ttlMs: options.issueOptions.ttlMs,
|
|
574
|
+
});
|
|
575
|
+
const wordCount =
|
|
576
|
+
options.issueOptions.strength === "high" ? HIGH_CHOICE_WORD_COUNT : STANDARD_CHOICE_WORD_COUNT;
|
|
577
|
+
for (let attempt = 0; attempt < SERVER_STORED_CHOICE_ISSUE_ATTEMPTS; attempt += 1) {
|
|
578
|
+
const stateKey = generateChoiceWordSequence(wordCount);
|
|
579
|
+
const token = `${options.issueOptions.prefix}${stateKey}`;
|
|
580
|
+
const record: ServerStoredChoiceRecord = {
|
|
581
|
+
v: SERVER_STORED_CHOICE_RECORD_VERSION,
|
|
582
|
+
storage: "server",
|
|
583
|
+
status: "active",
|
|
584
|
+
provider_id: options.baseEnvelope.provider_id,
|
|
585
|
+
purpose: options.baseEnvelope.purpose,
|
|
586
|
+
issued_at_ms: options.baseEnvelope.issued_at_ms,
|
|
587
|
+
ttl_ms: options.baseEnvelope.ttl_ms,
|
|
588
|
+
binding: options.baseEnvelope.binding,
|
|
589
|
+
prefix: options.issueOptions.prefix,
|
|
590
|
+
payload: options.issueOptions.payload,
|
|
591
|
+
payload_digest: payloadDigest,
|
|
592
|
+
replay_key: digestChoiceReplayKey(token),
|
|
593
|
+
};
|
|
594
|
+
const valueBytes = Buffer.byteLength(JSON.stringify(record), "utf8");
|
|
595
|
+
if (valueBytes > options.storage.maxValueBytes) {
|
|
596
|
+
throw new ProviderError("Provider choice payload exceeds state storage policy.", {
|
|
362
597
|
code: "CHOICE_STATE_PAYLOAD_TOO_LARGE",
|
|
363
598
|
category: "input_validation",
|
|
364
599
|
retryable: false,
|
|
365
600
|
details: {
|
|
366
601
|
maxValueBytes: options.storage.maxValueBytes,
|
|
367
|
-
|
|
602
|
+
valueBytes,
|
|
368
603
|
},
|
|
369
|
-
}
|
|
370
|
-
|
|
604
|
+
});
|
|
605
|
+
}
|
|
606
|
+
const result = await namespace.compareAndSet(optionsStateKey(stateKey), 0, record, {
|
|
607
|
+
ttl: stateTtl(options.storage, options.issueOptions.ttlMs),
|
|
608
|
+
});
|
|
609
|
+
if (result.ok) return token;
|
|
371
610
|
}
|
|
372
|
-
|
|
373
|
-
|
|
611
|
+
throw new ProviderError("Provider choice state storage is not available.", {
|
|
612
|
+
code: "CHOICE_STATE_UNAVAILABLE",
|
|
613
|
+
category: "internal_error",
|
|
614
|
+
retryable: false,
|
|
615
|
+
});
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
async function parseWordServerStoredChoice(options: {
|
|
619
|
+
readonly stateKey: string;
|
|
620
|
+
readonly parseOptions: ProviderChoiceParseOptions;
|
|
621
|
+
readonly contextState?: ProviderRuntimeState;
|
|
622
|
+
readonly providerId: string;
|
|
623
|
+
readonly request?: ProviderRequestContext;
|
|
624
|
+
readonly credential?: CredentialContext;
|
|
625
|
+
readonly resolveBindingKeys: () => ManagedChoiceKeys;
|
|
626
|
+
readonly onConsume: (result: ProviderChoiceConsumeResult) => void;
|
|
627
|
+
}): Promise<ProviderChoiceTokenPayload | ProviderChoiceExplicitParseResult> {
|
|
628
|
+
const storage = resolveParseStorage(options.parseOptions.storage);
|
|
374
629
|
const namespace = resolveChoiceStateNamespace({
|
|
375
|
-
storage
|
|
630
|
+
storage,
|
|
376
631
|
contextState: options.contextState,
|
|
377
|
-
ttlMs: options.
|
|
632
|
+
ttlMs: options.parseOptions.ttlMs,
|
|
378
633
|
});
|
|
379
|
-
|
|
380
|
-
|
|
634
|
+
let stored: StateValue<ServerStoredChoiceRecord> | null;
|
|
635
|
+
try {
|
|
636
|
+
stored = await namespace.get<ServerStoredChoiceRecord>(optionsStateKey(options.stateKey));
|
|
637
|
+
} catch (error) {
|
|
638
|
+
if (isProviderError(error)) throw error;
|
|
639
|
+
throw wordChoiceNotFoundError();
|
|
640
|
+
}
|
|
641
|
+
if (!stored || !isServerStoredChoiceRecord(stored.value)) {
|
|
642
|
+
throw wordChoiceNotFoundError();
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
const record = stored.value;
|
|
646
|
+
const expectedReplayKey = digestChoiceReplayKey(
|
|
647
|
+
`${options.parseOptions.prefix}${options.stateKey}`,
|
|
648
|
+
);
|
|
649
|
+
try {
|
|
650
|
+
if (
|
|
651
|
+
record.provider_id !== options.providerId ||
|
|
652
|
+
record.purpose !== options.parseOptions.purpose ||
|
|
653
|
+
record.prefix !== options.parseOptions.prefix
|
|
654
|
+
) {
|
|
655
|
+
throw wordChoiceNotFoundError();
|
|
656
|
+
}
|
|
657
|
+
assertFreshProviderChoiceIssuedAt(record.issued_at_ms, {
|
|
658
|
+
ttlMs:
|
|
659
|
+
options.parseOptions.ttlMs != null
|
|
660
|
+
? Math.min(options.parseOptions.ttlMs, record.ttl_ms)
|
|
661
|
+
: record.ttl_ms,
|
|
662
|
+
nowMs: options.parseOptions.nowMs,
|
|
663
|
+
futureToleranceMs: options.parseOptions.futureToleranceMs,
|
|
664
|
+
});
|
|
665
|
+
assertPayloadDigestMatches({
|
|
666
|
+
actual: digestChoicePayload(serializeChoicePayload(record.payload)),
|
|
667
|
+
expected: record.payload_digest,
|
|
668
|
+
});
|
|
669
|
+
assertPayloadDigestMatches({ actual: expectedReplayKey, expected: record.replay_key });
|
|
670
|
+
assertWordChoiceBindingMatches({
|
|
671
|
+
actual: record.binding,
|
|
672
|
+
requested: options.parseOptions.bind,
|
|
673
|
+
request: options.request,
|
|
674
|
+
credential: options.credential,
|
|
675
|
+
resolveKeys: options.resolveBindingKeys,
|
|
676
|
+
});
|
|
677
|
+
} catch (error) {
|
|
678
|
+
if (
|
|
679
|
+
error instanceof ProviderChoiceTokenError ||
|
|
680
|
+
(isProviderError(error) && error.code === "CHOICE_CONTEXT_REQUIRED")
|
|
681
|
+
) {
|
|
682
|
+
throw wordChoiceNotFoundError();
|
|
683
|
+
}
|
|
684
|
+
throw error;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
const consumeMode = options.parseOptions.consume ?? "never";
|
|
688
|
+
if (record.status === "consumed") {
|
|
689
|
+
if (consumeMode === "explicit") {
|
|
690
|
+
return { status: "consumed", replayKey: record.replay_key };
|
|
691
|
+
}
|
|
692
|
+
throw wordChoiceNotFoundError();
|
|
693
|
+
}
|
|
694
|
+
if (consumeMode === "never") return record.payload;
|
|
695
|
+
if (consumeMode === "explicit") {
|
|
696
|
+
return {
|
|
697
|
+
status: "active",
|
|
698
|
+
payload: record.payload,
|
|
699
|
+
replayKey: record.replay_key,
|
|
700
|
+
consume: async () => {
|
|
701
|
+
const result = await consumeWordServerStoredChoice({
|
|
702
|
+
stateKey: options.stateKey,
|
|
703
|
+
stored,
|
|
704
|
+
record,
|
|
705
|
+
storage,
|
|
706
|
+
contextState: options.contextState,
|
|
707
|
+
});
|
|
708
|
+
options.onConsume(result);
|
|
709
|
+
return result;
|
|
710
|
+
},
|
|
711
|
+
};
|
|
712
|
+
}
|
|
713
|
+
const consumed = await consumeWordServerStoredChoice({
|
|
714
|
+
stateKey: options.stateKey,
|
|
715
|
+
stored,
|
|
716
|
+
record,
|
|
717
|
+
storage,
|
|
718
|
+
contextState: options.contextState,
|
|
381
719
|
});
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
720
|
+
if (consumed.status !== "consumed") throw wordChoiceNotFoundError();
|
|
721
|
+
return record.payload;
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
async function consumeWordServerStoredChoice(options: {
|
|
725
|
+
readonly stateKey: string;
|
|
726
|
+
readonly stored: StateValue<ServerStoredChoiceRecord>;
|
|
727
|
+
readonly record: ServerStoredChoiceRecord;
|
|
728
|
+
readonly storage: ServerProviderChoiceStorageOptions;
|
|
729
|
+
readonly contextState?: ProviderRuntimeState;
|
|
730
|
+
}): Promise<ProviderChoiceConsumeResult> {
|
|
731
|
+
const namespace = resolveChoiceStateNamespace({
|
|
732
|
+
storage: options.storage,
|
|
733
|
+
contextState: options.contextState,
|
|
734
|
+
ttlMs: options.record.ttl_ms,
|
|
396
735
|
});
|
|
736
|
+
try {
|
|
737
|
+
const consumed = await namespace.compareAndSet(
|
|
738
|
+
optionsStateKey(options.stateKey),
|
|
739
|
+
options.stored.version,
|
|
740
|
+
{ ...options.record, status: "consumed" } satisfies ServerStoredChoiceRecord,
|
|
741
|
+
{ ttl: remainingStateTtl(options.stored.expiresAt) },
|
|
742
|
+
);
|
|
743
|
+
if (consumed.ok) return { status: "consumed" };
|
|
744
|
+
if (
|
|
745
|
+
consumed.current &&
|
|
746
|
+
isServerStoredChoiceRecord(consumed.current.value) &&
|
|
747
|
+
consumed.current.value.status === "consumed" &&
|
|
748
|
+
consumed.current.value.replay_key === options.record.replay_key
|
|
749
|
+
) {
|
|
750
|
+
return { status: "already-consumed" };
|
|
751
|
+
}
|
|
752
|
+
throw wordChoiceNotFoundError();
|
|
753
|
+
} catch (error) {
|
|
754
|
+
if (isProviderError(error)) throw error;
|
|
755
|
+
if (error instanceof ProviderChoiceTokenError) throw error;
|
|
756
|
+
throw wordChoiceNotFoundError();
|
|
757
|
+
}
|
|
397
758
|
}
|
|
398
759
|
|
|
399
|
-
async function
|
|
760
|
+
async function parseLegacyServerStoredChoice(options: {
|
|
400
761
|
readonly handle: ServerChoiceHandlePayload;
|
|
401
762
|
readonly storage?: ProviderChoiceStorageOptions;
|
|
402
763
|
readonly contextState?: ProviderRuntimeState;
|
|
@@ -406,9 +767,29 @@ async function parseServerStoredChoice(options: {
|
|
|
406
767
|
storage,
|
|
407
768
|
contextState: options.contextState,
|
|
408
769
|
});
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
)
|
|
770
|
+
// Reading a server-stored choice back deserializes a persisted value. A
|
|
771
|
+
// corrupt/undecodable value would otherwise surface as a raw JSON.parse
|
|
772
|
+
// SyntaxError (or another unexpected throwable) that escapes the choice error
|
|
773
|
+
// taxonomy, gets masked as internal_error 500, and is treated as retryable by
|
|
774
|
+
// the hub -> reservation restart loop (2026-07-22 catchtable RCA, candidate A).
|
|
775
|
+
// Convert any non-branded throwable into a branded invalid_payload so it maps
|
|
776
|
+
// to a clean, non-retryable 400. Branded ProviderChoiceTokenError and genuine
|
|
777
|
+
// ProviderError (e.g. Redis-unavailable / state-unavailable) pass through so
|
|
778
|
+
// their category/retryable semantics are preserved.
|
|
779
|
+
let record: StateValue<ProviderChoiceTokenPayload> | null;
|
|
780
|
+
try {
|
|
781
|
+
record = await namespace.get<ProviderChoiceTokenPayload>(
|
|
782
|
+
optionsStateKey(options.handle.state_id),
|
|
783
|
+
);
|
|
784
|
+
} catch (error) {
|
|
785
|
+
if (error instanceof ProviderChoiceTokenError || isProviderError(error)) {
|
|
786
|
+
throw error;
|
|
787
|
+
}
|
|
788
|
+
throw new ProviderChoiceTokenError(
|
|
789
|
+
"invalid_payload",
|
|
790
|
+
"Provider choice token state payload could not be decoded.",
|
|
791
|
+
);
|
|
792
|
+
}
|
|
412
793
|
if (!record) {
|
|
413
794
|
throw new ProviderChoiceTokenError(
|
|
414
795
|
"invalid_payload",
|
|
@@ -423,6 +804,83 @@ async function parseServerStoredChoice(options: {
|
|
|
423
804
|
return record.value;
|
|
424
805
|
}
|
|
425
806
|
|
|
807
|
+
function generateChoiceWordSequence(wordCount: number): string {
|
|
808
|
+
return Array.from({ length: wordCount }, () =>
|
|
809
|
+
choiceWordAt(randomInt(CHOICE_WORDLIST_SIZE)),
|
|
810
|
+
).join("-");
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
function parseWordChoiceStateKey(options: {
|
|
814
|
+
readonly token: string;
|
|
815
|
+
readonly prefix: string;
|
|
816
|
+
}): string | null {
|
|
817
|
+
if (!options.token.startsWith(options.prefix)) return null;
|
|
818
|
+
const body = options.token.slice(options.prefix.length);
|
|
819
|
+
// The official list contains one hyphenated entry (`yo-yo`), so structural
|
|
820
|
+
// recognition uses dictionary-aware segmentation instead of assuming every
|
|
821
|
+
// hyphen is a word boundary.
|
|
822
|
+
if (!/^[a-z]+(?:-[a-z]+){3,9}$/.test(body)) return null;
|
|
823
|
+
const segments = body.split("-");
|
|
824
|
+
if (
|
|
825
|
+
!canSegmentChoiceWords(segments, 0, STANDARD_CHOICE_WORD_COUNT) &&
|
|
826
|
+
!canSegmentChoiceWords(segments, 0, HIGH_CHOICE_WORD_COUNT)
|
|
827
|
+
) {
|
|
828
|
+
return null;
|
|
829
|
+
}
|
|
830
|
+
return body;
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
function canSegmentChoiceWords(
|
|
834
|
+
segments: readonly string[],
|
|
835
|
+
segmentIndex: number,
|
|
836
|
+
wordsRemaining: number,
|
|
837
|
+
): boolean {
|
|
838
|
+
if (wordsRemaining === 0) return segmentIndex === segments.length;
|
|
839
|
+
const segmentsRemaining = segments.length - segmentIndex;
|
|
840
|
+
if (segmentsRemaining < wordsRemaining) return false;
|
|
841
|
+
for (let end = segmentIndex + 1; end <= segments.length - (wordsRemaining - 1); end += 1) {
|
|
842
|
+
const candidate = segments.slice(segmentIndex, end).join("-");
|
|
843
|
+
if (candidate.length > 10) break;
|
|
844
|
+
if (isChoiceWord(candidate) && canSegmentChoiceWords(segments, end, wordsRemaining - 1)) {
|
|
845
|
+
return true;
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
return false;
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
function isServerStoredChoiceRecord(value: unknown): value is ServerStoredChoiceRecord {
|
|
852
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
853
|
+
return (
|
|
854
|
+
"v" in value &&
|
|
855
|
+
value.v === SERVER_STORED_CHOICE_RECORD_VERSION &&
|
|
856
|
+
"storage" in value &&
|
|
857
|
+
value.storage === "server" &&
|
|
858
|
+
"status" in value &&
|
|
859
|
+
(value.status === "active" || value.status === "consumed") &&
|
|
860
|
+
"provider_id" in value &&
|
|
861
|
+
typeof value.provider_id === "string" &&
|
|
862
|
+
"purpose" in value &&
|
|
863
|
+
typeof value.purpose === "string" &&
|
|
864
|
+
"issued_at_ms" in value &&
|
|
865
|
+
typeof value.issued_at_ms === "number" &&
|
|
866
|
+
"ttl_ms" in value &&
|
|
867
|
+
typeof value.ttl_ms === "number" &&
|
|
868
|
+
(!("binding" in value) || value.binding === undefined || isChoiceBinding(value.binding)) &&
|
|
869
|
+
"prefix" in value &&
|
|
870
|
+
typeof value.prefix === "string" &&
|
|
871
|
+
"payload" in value &&
|
|
872
|
+
isChoicePayload(value.payload) &&
|
|
873
|
+
"payload_digest" in value &&
|
|
874
|
+
typeof value.payload_digest === "string" &&
|
|
875
|
+
"replay_key" in value &&
|
|
876
|
+
typeof value.replay_key === "string"
|
|
877
|
+
);
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
function wordChoiceNotFoundError(): ProviderChoiceTokenError {
|
|
881
|
+
return new ProviderChoiceTokenError("invalid_payload", WORD_CHOICE_NOT_FOUND_MESSAGE);
|
|
882
|
+
}
|
|
883
|
+
|
|
426
884
|
type ServerProviderChoiceStorageOptions = Extract<
|
|
427
885
|
ProviderChoiceStorageOptions,
|
|
428
886
|
{ readonly mode: "server" | "auto" }
|
|
@@ -439,10 +897,7 @@ function resolveIssueStorage<TPayload extends ProviderChoiceTokenPayload>(
|
|
|
439
897
|
} {
|
|
440
898
|
if (!storage || storage.mode === "inline") return { mode: "inline" };
|
|
441
899
|
if (storage.mode === "server") return { mode: "server", storage };
|
|
442
|
-
const payloadBytes = Buffer.byteLength(
|
|
443
|
-
serializeChoicePayload(payload),
|
|
444
|
-
"utf8",
|
|
445
|
-
);
|
|
900
|
+
const payloadBytes = Buffer.byteLength(serializeChoicePayload(payload), "utf8");
|
|
446
901
|
if (payloadBytes <= storage.maxInlineBytes) return { mode: "inline" };
|
|
447
902
|
return { mode: "server", storage };
|
|
448
903
|
}
|
|
@@ -487,6 +942,11 @@ function stateTtl(
|
|
|
487
942
|
return storage.ttl ?? `${ttlMs ?? 1}ms`;
|
|
488
943
|
}
|
|
489
944
|
|
|
945
|
+
function remainingStateTtl(expiresAt: string): ProviderStateDurationString {
|
|
946
|
+
const remainingMs = Date.parse(expiresAt) - Date.now();
|
|
947
|
+
return `${Number.isFinite(remainingMs) ? Math.max(1, Math.floor(remainingMs)) : 1}ms`;
|
|
948
|
+
}
|
|
949
|
+
|
|
490
950
|
function optionsStateKey(stateId: string): string {
|
|
491
951
|
return stateId;
|
|
492
952
|
}
|
|
@@ -499,6 +959,10 @@ function digestChoicePayload(serializedPayload: string): string {
|
|
|
499
959
|
return createHash("sha256").update(serializedPayload).digest("base64url");
|
|
500
960
|
}
|
|
501
961
|
|
|
962
|
+
function digestChoiceReplayKey(token: string): string {
|
|
963
|
+
return createHash("sha256").update(token).digest("hex");
|
|
964
|
+
}
|
|
965
|
+
|
|
502
966
|
function isServerChoiceHandlePayload(
|
|
503
967
|
value: ProviderChoiceTokenPayload,
|
|
504
968
|
): value is ServerChoiceHandlePayload {
|
|
@@ -536,10 +1000,7 @@ function parseManagedChoiceTokenParts(
|
|
|
536
1000
|
] {
|
|
537
1001
|
const parts = token.split(".");
|
|
538
1002
|
if (parts.length !== 6) {
|
|
539
|
-
throw new ProviderChoiceTokenError(
|
|
540
|
-
"invalid_shape",
|
|
541
|
-
"Provider choice token shape is invalid.",
|
|
542
|
-
);
|
|
1003
|
+
throw new ProviderChoiceTokenError("invalid_shape", "Provider choice token shape is invalid.");
|
|
543
1004
|
}
|
|
544
1005
|
return [parts[0], parts[1], parts[2], parts[3], parts[4], parts[5]];
|
|
545
1006
|
}
|
|
@@ -601,9 +1062,7 @@ function decryptManagedChoiceToken(options: {
|
|
|
601
1062
|
}
|
|
602
1063
|
}
|
|
603
1064
|
|
|
604
|
-
function isManagedChoiceEnvelope(
|
|
605
|
-
value: unknown,
|
|
606
|
-
): value is ManagedChoiceEnvelope {
|
|
1065
|
+
function isManagedChoiceEnvelope(value: unknown): value is ManagedChoiceEnvelope {
|
|
607
1066
|
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
608
1067
|
if (!("payload" in value) || !isChoicePayload(value.payload)) return false;
|
|
609
1068
|
return (
|
|
@@ -617,9 +1076,7 @@ function isManagedChoiceEnvelope(
|
|
|
617
1076
|
typeof value.issued_at_ms === "number" &&
|
|
618
1077
|
"ttl_ms" in value &&
|
|
619
1078
|
typeof value.ttl_ms === "number" &&
|
|
620
|
-
(!("binding" in value) ||
|
|
621
|
-
value.binding === undefined ||
|
|
622
|
-
isChoiceBinding(value.binding))
|
|
1079
|
+
(!("binding" in value) || value.binding === undefined || isChoiceBinding(value.binding))
|
|
623
1080
|
);
|
|
624
1081
|
}
|
|
625
1082
|
|
|
@@ -627,13 +1084,10 @@ function isChoicePayload(value: unknown): value is ProviderChoiceTokenPayload {
|
|
|
627
1084
|
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
628
1085
|
}
|
|
629
1086
|
|
|
630
|
-
function isChoiceBinding(
|
|
631
|
-
value: unknown,
|
|
632
|
-
): value is ManagedChoiceEnvelope["binding"] {
|
|
1087
|
+
function isChoiceBinding(value: unknown): value is ManagedChoiceEnvelope["binding"] {
|
|
633
1088
|
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
634
1089
|
return (
|
|
635
|
-
(!("connection_hash" in value) ||
|
|
636
|
-
typeof value.connection_hash === "string") &&
|
|
1090
|
+
(!("connection_hash" in value) || typeof value.connection_hash === "string") &&
|
|
637
1091
|
(!("credential_hash" in value) || typeof value.credential_hash === "string")
|
|
638
1092
|
);
|
|
639
1093
|
}
|
|
@@ -648,10 +1102,7 @@ function assertManagedChoiceEnvelope(
|
|
|
648
1102
|
readonly futureToleranceMs?: number;
|
|
649
1103
|
},
|
|
650
1104
|
): void {
|
|
651
|
-
if (
|
|
652
|
-
envelope.provider_id !== options.providerId ||
|
|
653
|
-
envelope.purpose !== options.purpose
|
|
654
|
-
) {
|
|
1105
|
+
if (envelope.provider_id !== options.providerId || envelope.purpose !== options.purpose) {
|
|
655
1106
|
throw new ProviderChoiceTokenError(
|
|
656
1107
|
"invalid_payload",
|
|
657
1108
|
"Provider choice token payload is invalid.",
|
|
@@ -660,10 +1111,7 @@ function assertManagedChoiceEnvelope(
|
|
|
660
1111
|
assertFreshProviderChoiceIssuedAt(envelope.issued_at_ms, {
|
|
661
1112
|
// Clamp to the issuer's embedded TTL so a caller-supplied value cannot
|
|
662
1113
|
// silently extend token validity past the deadline the issuer intended.
|
|
663
|
-
ttlMs:
|
|
664
|
-
options.ttlMs != null
|
|
665
|
-
? Math.min(options.ttlMs, envelope.ttl_ms)
|
|
666
|
-
: envelope.ttl_ms,
|
|
1114
|
+
ttlMs: options.ttlMs != null ? Math.min(options.ttlMs, envelope.ttl_ms) : envelope.ttl_ms,
|
|
667
1115
|
nowMs: options.nowMs,
|
|
668
1116
|
futureToleranceMs: options.futureToleranceMs,
|
|
669
1117
|
});
|
|
@@ -676,9 +1124,7 @@ function createChoiceBinding(options: {
|
|
|
676
1124
|
readonly credential?: CredentialContext;
|
|
677
1125
|
readonly required: boolean;
|
|
678
1126
|
}): ManagedChoiceEnvelope["binding"] {
|
|
679
|
-
const connectionHash = options.options?.connection
|
|
680
|
-
? hashRequiredConnection(options)
|
|
681
|
-
: undefined;
|
|
1127
|
+
const connectionHash = options.options?.connection ? hashRequiredConnection(options) : undefined;
|
|
682
1128
|
const credentialHash = options.options?.credentialKeys?.length
|
|
683
1129
|
? hashCredentialKeys(options)
|
|
684
1130
|
: undefined;
|
|
@@ -689,6 +1135,35 @@ function createChoiceBinding(options: {
|
|
|
689
1135
|
};
|
|
690
1136
|
}
|
|
691
1137
|
|
|
1138
|
+
function hasRequestedChoiceBinding(options?: ProviderChoiceBindingOptions): boolean {
|
|
1139
|
+
return options?.connection === true || Boolean(options?.credentialKeys?.length);
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
function assertWordChoiceBindingMatches(options: {
|
|
1143
|
+
readonly actual: ManagedChoiceEnvelope["binding"];
|
|
1144
|
+
readonly requested?: ProviderChoiceBindingOptions;
|
|
1145
|
+
readonly request?: ProviderRequestContext;
|
|
1146
|
+
readonly credential?: CredentialContext;
|
|
1147
|
+
readonly resolveKeys: () => ManagedChoiceKeys;
|
|
1148
|
+
}): void {
|
|
1149
|
+
const hasStoredBinding = Boolean(
|
|
1150
|
+
options.actual?.connection_hash || options.actual?.credential_hash,
|
|
1151
|
+
);
|
|
1152
|
+
const hasRequestedBinding = hasRequestedChoiceBinding(options.requested);
|
|
1153
|
+
if (!hasStoredBinding && !hasRequestedBinding) return;
|
|
1154
|
+
if (hasStoredBinding !== hasRequestedBinding) throw wordChoiceNotFoundError();
|
|
1155
|
+
assertChoiceBindingMatches({
|
|
1156
|
+
actual: options.actual,
|
|
1157
|
+
expected: createChoiceBinding({
|
|
1158
|
+
keys: options.resolveKeys(),
|
|
1159
|
+
options: options.requested,
|
|
1160
|
+
request: options.request,
|
|
1161
|
+
credential: options.credential,
|
|
1162
|
+
required: true,
|
|
1163
|
+
}),
|
|
1164
|
+
});
|
|
1165
|
+
}
|
|
1166
|
+
|
|
692
1167
|
function hashRequiredConnection(options: {
|
|
693
1168
|
readonly keys: ManagedChoiceKeys;
|
|
694
1169
|
readonly request?: ProviderRequestContext;
|
|
@@ -697,14 +1172,11 @@ function hashRequiredConnection(options: {
|
|
|
697
1172
|
const connectionId = options.request?.connectionId;
|
|
698
1173
|
if (!connectionId) {
|
|
699
1174
|
if (!options.required) return undefined;
|
|
700
|
-
throw new ProviderError(
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
retryable: false,
|
|
706
|
-
},
|
|
707
|
-
);
|
|
1175
|
+
throw new ProviderError("Provider choice tokens require connection context.", {
|
|
1176
|
+
code: "CHOICE_CONTEXT_REQUIRED",
|
|
1177
|
+
category: "input_validation",
|
|
1178
|
+
retryable: false,
|
|
1179
|
+
});
|
|
708
1180
|
}
|
|
709
1181
|
return createHmac("sha256", options.keys.binding)
|
|
710
1182
|
.update("connection")
|
|
@@ -722,15 +1194,12 @@ function hashCredentialKeys(options: {
|
|
|
722
1194
|
const material = credentialKeys.map((key) => {
|
|
723
1195
|
const value = options.credential?.get(key);
|
|
724
1196
|
if (typeof value !== "string" || value.length === 0) {
|
|
725
|
-
throw new ProviderError(
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
details: { credentialKey: key },
|
|
732
|
-
},
|
|
733
|
-
);
|
|
1197
|
+
throw new ProviderError("Provider choice tokens require configured credential binding.", {
|
|
1198
|
+
code: "CHOICE_CONTEXT_REQUIRED",
|
|
1199
|
+
category: "input_validation",
|
|
1200
|
+
retryable: false,
|
|
1201
|
+
details: { credentialKey: key },
|
|
1202
|
+
});
|
|
734
1203
|
}
|
|
735
1204
|
return [key, value];
|
|
736
1205
|
});
|