@apifuse/provider-sdk 2.2.0-beta.3 → 2.2.0-beta.31
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AUTHORING.md +493 -5
- package/CHANGELOG.md +131 -1
- package/README.md +52 -6
- package/SUBMISSION.md +1 -1
- package/bin/apifuse-check.ts +106 -62
- package/bin/apifuse-create.ts +1 -1
- package/bin/apifuse-dev.ts +63 -55
- package/bin/apifuse-pack-check.ts +22 -2
- package/bin/apifuse-pack-smoke.ts +78 -82
- package/bin/apifuse-pack-types.ts +583 -0
- package/bin/apifuse-perf.ts +59 -140
- package/bin/apifuse-record.ts +698 -113
- package/bin/apifuse-submit-check.ts +517 -44
- package/bin/apifuse-sync-assets.ts +117 -0
- package/bin/apifuse.ts +1 -1
- package/bin/submit-check-delimited-text.ts +50 -0
- package/bin/submit-check-xml.ts +1 -1
- package/dist/auth-turn/index.d.ts +4 -4
- package/dist/auth-turn/index.js +1 -1
- package/dist/auth.d.ts +16 -2
- package/dist/auth.js +76 -18
- package/dist/ceremonies/index.d.ts +9 -1
- package/dist/ceremonies/index.js +117 -29
- package/dist/cli/commands.d.ts +1 -1
- package/dist/cli/commands.js +8 -0
- package/dist/cli/create.d.ts +3 -0
- package/dist/cli/create.js +34 -35
- package/dist/cli/prompt-assets.d.ts +80 -0
- package/dist/cli/prompt-assets.js +743 -0
- package/dist/cli/templates/provider/AGENTS.md.tpl +17 -8
- package/dist/cli/templates/provider/README.md.tpl +4 -4
- package/dist/config/loader.d.ts +176 -17
- package/dist/config/loader.js +434 -161
- package/dist/contract-serialization.d.ts +2 -2
- package/dist/contract-serialization.js +7 -14
- package/dist/contract-types.d.ts +3 -2
- package/dist/contract.d.ts +3 -3
- package/dist/contract.js +6 -6
- package/dist/declaration-validation.d.ts +23 -0
- package/dist/declaration-validation.js +159 -0
- package/dist/define.d.ts +13 -1
- package/dist/define.js +391 -122
- package/dist/dev.d.ts +1 -1
- package/dist/dev.js +1 -1
- package/dist/error-resolution.d.ts +4 -0
- package/dist/error-resolution.js +123 -0
- package/dist/errors.d.ts +19 -1
- package/dist/errors.js +41 -3
- package/dist/fixture-sanitization.d.ts +26 -0
- package/dist/fixture-sanitization.js +216 -0
- package/dist/i18n/catalog.d.ts +2 -2
- package/dist/i18n/catalog.js +4 -10
- package/dist/i18n/index.d.ts +2 -2
- package/dist/i18n/index.js +2 -2
- package/dist/i18n/keys.d.ts +2 -2
- package/dist/index.d.ts +50 -42
- package/dist/index.js +41 -37
- package/dist/lint.d.ts +6 -1
- package/dist/lint.js +370 -18
- package/dist/native-address.d.ts +43 -0
- package/dist/native-address.js +281 -0
- package/dist/native-egress-policy.d.ts +31 -0
- package/dist/native-egress-policy.js +288 -0
- package/dist/observability.d.ts +5 -2
- package/dist/observability.js +48 -1
- package/dist/provider.d.ts +13 -11
- package/dist/provider.js +10 -9
- package/dist/public-schema-field-lint.d.ts +1 -1
- package/dist/recipes/gov-api.js +1 -1
- package/dist/runtime/auth-flow.d.ts +3 -1
- package/dist/runtime/auth-flow.js +8 -3
- package/dist/runtime/browser.d.ts +1 -1
- package/dist/runtime/browser.js +138 -40
- package/dist/runtime/cache.d.ts +2 -1
- package/dist/runtime/cache.js +173 -23
- package/dist/runtime/choice-wordlist.d.ts +9 -0
- package/dist/runtime/choice-wordlist.js +138 -0
- package/dist/runtime/choice.d.ts +14 -1
- package/dist/runtime/choice.js +566 -101
- package/dist/runtime/credential.d.ts +1 -1
- package/dist/runtime/credential.js +1 -1
- package/dist/runtime/env.d.ts +1 -1
- package/dist/runtime/executor.d.ts +1 -1
- package/dist/runtime/executor.js +25 -3
- package/dist/runtime/http.d.ts +3 -2
- package/dist/runtime/http.js +517 -55
- package/dist/runtime/insights.d.ts +1 -1
- package/dist/runtime/insights.js +6 -13
- package/dist/runtime/instrumentation.d.ts +2 -2
- package/dist/runtime/instrumentation.js +371 -23
- package/dist/runtime/keyring.js +1 -1
- package/dist/runtime/namespace.js +1 -1
- package/dist/runtime/native-network-errors.d.ts +33 -0
- package/dist/runtime/native-network-errors.js +69 -0
- package/dist/runtime/native-network.d.ts +96 -0
- package/dist/runtime/native-network.js +1232 -0
- package/dist/runtime/ocr.d.ts +29 -0
- package/dist/runtime/ocr.js +440 -0
- package/dist/runtime/otlp.d.ts +1 -1
- package/dist/runtime/perf.d.ts +1 -1
- package/dist/runtime/provider.d.ts +1 -1
- package/dist/runtime/provider.js +1 -2
- package/dist/runtime/proxy-errors.d.ts +1 -1
- package/dist/runtime/proxy-errors.js +9 -7
- package/dist/runtime/proxy-nodemaven.d.ts +56 -0
- package/dist/runtime/proxy-nodemaven.js +146 -0
- package/dist/runtime/proxy-retry-policy.d.ts +2 -2
- package/dist/runtime/proxy-retry-policy.js +2 -2
- package/dist/runtime/proxy-telemetry.d.ts +2 -1
- package/dist/runtime/proxy-telemetry.js +58 -52
- package/dist/runtime/redirects.d.ts +29 -0
- package/dist/runtime/redirects.js +36 -0
- package/dist/runtime/redis.d.ts +1 -1
- package/dist/runtime/redis.js +5 -5
- package/dist/runtime/request-options.d.ts +68 -1
- package/dist/runtime/request-options.js +548 -0
- package/dist/runtime/resolver-config.d.ts +6 -0
- package/dist/runtime/resolver-config.js +6 -0
- package/dist/runtime/resolver-public.d.ts +1 -0
- package/dist/runtime/resolver-public.js +1 -0
- package/dist/runtime/resolver-shared.d.ts +3 -0
- package/dist/runtime/resolver-shared.js +12 -0
- package/dist/runtime/resolver-vendors/bindings.d.ts +48 -0
- package/dist/runtime/resolver-vendors/bindings.js +40 -0
- package/dist/runtime/resolver-vendors/browser.d.ts +20 -0
- package/dist/runtime/resolver-vendors/browser.js +282 -0
- package/dist/runtime/resolver-vendors/hosts.d.ts +2 -0
- package/dist/runtime/resolver-vendors/hosts.js +33 -0
- package/dist/runtime/resolver-vendors/twocaptcha.d.ts +24 -0
- package/dist/runtime/resolver-vendors/twocaptcha.js +368 -0
- package/dist/runtime/resolver-vendors/types.d.ts +83 -0
- package/dist/runtime/resolver-vendors/types.js +69 -0
- package/dist/runtime/resolver.d.ts +59 -0
- package/dist/runtime/resolver.js +705 -0
- package/dist/runtime/secrets.d.ts +27 -0
- package/dist/runtime/secrets.js +51 -0
- package/dist/runtime/state.d.ts +5 -2
- package/dist/runtime/state.js +280 -74
- package/dist/runtime/stealth-cookies.d.ts +20 -0
- package/dist/runtime/stealth-cookies.js +111 -0
- package/dist/runtime/stealth.d.ts +30 -5
- package/dist/runtime/stealth.js +523 -259
- package/dist/runtime/stt.d.ts +1 -1
- package/dist/runtime/stt.js +12 -27
- package/dist/runtime/timeout.d.ts +5 -0
- package/dist/runtime/timeout.js +12 -0
- package/dist/runtime/trace.d.ts +2 -2
- package/dist/runtime/trace.js +2 -4
- package/dist/runtime/waterfall.d.ts +1 -1
- package/dist/schema.d.ts +1 -1
- package/dist/schema.js +7 -15
- package/dist/serve.d.ts +1 -1
- package/dist/serve.js +1 -1
- package/dist/server/index.d.ts +7 -7
- package/dist/server/index.js +6 -6
- package/dist/server/self-test-input-tokens.d.ts +2 -1
- package/dist/server/self-test-input-tokens.js +18 -14
- package/dist/server/self-test-redaction.d.ts +1 -1
- package/dist/server/self-test-redaction.js +1 -1
- package/dist/server/self-test.d.ts +117 -3
- package/dist/server/self-test.js +787 -151
- package/dist/server/serve-implementation.d.ts +210 -0
- package/dist/server/serve-implementation.js +2078 -0
- package/dist/server/serve.d.ts +1 -70
- package/dist/server/serve.js +1 -1143
- package/dist/server/types.d.ts +34 -9
- package/dist/server/types.js +8 -1
- package/dist/stateful/errors.d.ts +19 -0
- package/dist/stateful/errors.js +24 -0
- package/dist/stateful/http-provider-event-emitter.d.ts +40 -0
- package/dist/stateful/http-provider-event-emitter.js +237 -0
- package/dist/stateful/http-session-owner-registry.d.ts +44 -0
- package/dist/stateful/http-session-owner-registry.js +210 -0
- package/dist/stateful/index.d.ts +18 -0
- package/dist/stateful/index.js +18 -0
- package/dist/stateful/provider-event-delivery-failures.d.ts +32 -0
- package/dist/stateful/provider-event-delivery-failures.js +43 -0
- package/dist/stateful/provider-event-pipeline-metrics.d.ts +46 -0
- package/dist/stateful/provider-event-pipeline-metrics.js +48 -0
- package/dist/stateful/provider-event-pipeline.d.ts +50 -0
- package/dist/stateful/provider-event-pipeline.js +1 -0
- package/dist/stateful/provider-events.d.ts +101 -0
- package/dist/stateful/provider-events.js +289 -0
- package/dist/stateful/session-key.d.ts +15 -0
- package/dist/stateful/session-key.js +86 -0
- package/dist/stateful/stateful-provider-adapter-context.d.ts +5 -0
- package/dist/stateful/stateful-provider-adapter-context.js +42 -0
- package/dist/stateful/stateful-provider-adapter-metrics.d.ts +15 -0
- package/dist/stateful/stateful-provider-adapter-metrics.js +21 -0
- package/dist/stateful/stateful-provider-adapter.d.ts +98 -0
- package/dist/stateful/stateful-provider-adapter.js +287 -0
- package/dist/stateful/stateful-provider-observability.d.ts +62 -0
- package/dist/stateful/stateful-provider-observability.js +161 -0
- package/dist/stateful/stateful-provider-owner-forwarder.d.ts +41 -0
- package/dist/stateful/stateful-provider-owner-forwarder.js +215 -0
- package/dist/stateful/stateful-provider-runtime-context.d.ts +32 -0
- package/dist/stateful/stateful-provider-runtime-context.js +60 -0
- package/dist/stateful/stateful-provider-runtime-executor.d.ts +34 -0
- package/dist/stateful/stateful-provider-runtime-executor.js +52 -0
- package/dist/stateful/stateful-provider-session-routing.d.ts +67 -0
- package/dist/stateful/stateful-provider-session-routing.js +345 -0
- package/dist/stateful/stateful-provider-session-runtime.d.ts +98 -0
- package/dist/stateful/stateful-provider-session-runtime.js +245 -0
- package/dist/stateful-signing.d.ts +18 -0
- package/dist/stateful-signing.js +27 -0
- package/dist/stealth/profiles.d.ts +1 -1
- package/dist/stealth/profiles.js +21 -21
- package/dist/stream-evidence.d.ts +74 -0
- package/dist/stream-evidence.js +785 -0
- package/dist/stream.d.ts +1 -1
- package/dist/stream.js +7 -1
- package/dist/testing/index.d.ts +3 -2
- package/dist/testing/index.js +3 -2
- package/dist/testing/run.d.ts +32 -2
- package/dist/testing/run.js +488 -28
- package/dist/types.d.ts +566 -19
- package/dist/types.js +1 -0
- package/dist/user-input.d.ts +30 -0
- package/dist/user-input.js +66 -0
- package/package.json +42 -7
- package/src/auth-turn/index.ts +2 -2
- package/src/auth.ts +146 -86
- package/src/ceremonies/index.ts +167 -92
- package/src/cli/commands.ts +10 -0
- package/src/cli/create.ts +42 -35
- package/src/cli/prompt-assets.ts +865 -0
- package/src/cli/templates/provider/AGENTS.md.tpl +17 -8
- package/src/cli/templates/provider/README.md.tpl +4 -4
- package/src/config/loader.ts +667 -289
- package/src/contract-serialization.ts +10 -18
- package/src/contract-types.ts +3 -2
- package/src/contract.ts +14 -28
- package/src/declaration-validation.ts +202 -0
- package/src/define.ts +631 -495
- package/src/dev.ts +4 -9
- package/src/error-resolution.ts +128 -0
- package/src/errors.ts +56 -11
- package/src/fixture-sanitization.ts +247 -0
- package/src/i18n/catalog.ts +10 -32
- package/src/i18n/index.ts +2 -2
- package/src/i18n/keys.ts +5 -11
- package/src/index.ts +158 -44
- package/src/lint.ts +488 -154
- package/src/native-address.ts +340 -0
- package/src/native-egress-policy.ts +358 -0
- package/src/observability.ts +51 -1
- package/src/provider.ts +66 -11
- package/src/public-schema-field-lint.ts +7 -33
- package/src/recipes/gov-api.ts +2 -5
- package/src/runtime/auth-flow.ts +13 -7
- package/src/runtime/browser.ts +252 -207
- package/src/runtime/cache.ts +209 -81
- package/src/runtime/choice-wordlist.ts +145 -0
- package/src/runtime/choice.ts +758 -197
- package/src/runtime/credential.ts +2 -2
- package/src/runtime/env.ts +1 -1
- package/src/runtime/executor.ts +37 -19
- package/src/runtime/http.ts +645 -65
- package/src/runtime/insights.ts +15 -53
- package/src/runtime/instrumentation.ts +530 -67
- package/src/runtime/keyring.ts +7 -19
- package/src/runtime/namespace.ts +2 -7
- package/src/runtime/native-network-errors.ts +99 -0
- package/src/runtime/native-network.ts +1605 -0
- package/src/runtime/ocr.ts +523 -0
- package/src/runtime/otlp.ts +12 -23
- package/src/runtime/perf.ts +1 -1
- package/src/runtime/provider.ts +4 -9
- package/src/runtime/proxy-errors.ts +29 -42
- package/src/runtime/proxy-nodemaven.ts +221 -0
- package/src/runtime/proxy-retry-policy.ts +3 -3
- package/src/runtime/proxy-telemetry.ts +84 -77
- package/src/runtime/redirects.ts +66 -0
- package/src/runtime/redis.ts +10 -13
- package/src/runtime/request-options.ts +679 -9
- package/src/runtime/resolver-config.ts +6 -0
- package/src/runtime/resolver-public.ts +18 -0
- package/src/runtime/resolver-shared.ts +17 -0
- package/src/runtime/resolver-vendors/bindings.ts +56 -0
- package/src/runtime/resolver-vendors/browser.ts +408 -0
- package/src/runtime/resolver-vendors/hosts.ts +38 -0
- package/src/runtime/resolver-vendors/twocaptcha.ts +500 -0
- package/src/runtime/resolver-vendors/types.ts +173 -0
- package/src/runtime/resolver.ts +1060 -0
- package/src/runtime/secrets.ts +64 -0
- package/src/runtime/state.ts +399 -161
- package/src/runtime/stealth-cookies.ts +132 -0
- package/src/runtime/stealth.ts +681 -295
- package/src/runtime/stt.ts +39 -113
- package/src/runtime/timeout.ts +18 -0
- package/src/runtime/trace.ts +14 -44
- package/src/runtime/waterfall.ts +5 -18
- package/src/schema.ts +23 -84
- package/src/serve.ts +6 -1
- package/src/server/index.ts +30 -7
- package/src/server/self-test-input-tokens.ts +29 -14
- package/src/server/self-test-redaction.ts +2 -2
- package/src/server/self-test.ts +1030 -180
- package/src/server/serve-implementation.ts +3062 -0
- package/src/server/serve.ts +1 -1781
- package/src/server/types.ts +12 -13
- package/src/stateful/README.md +146 -0
- package/src/stateful/errors.ts +35 -0
- package/src/stateful/http-provider-event-emitter.ts +314 -0
- package/src/stateful/http-session-owner-registry.ts +306 -0
- package/src/stateful/index.ts +18 -0
- package/src/stateful/provider-event-delivery-failures.ts +80 -0
- package/src/stateful/provider-event-pipeline-metrics.ts +95 -0
- package/src/stateful/provider-event-pipeline.ts +61 -0
- package/src/stateful/provider-events.ts +462 -0
- package/src/stateful/session-key.ts +111 -0
- package/src/stateful/stateful-provider-adapter-context.ts +59 -0
- package/src/stateful/stateful-provider-adapter-metrics.ts +48 -0
- package/src/stateful/stateful-provider-adapter.ts +562 -0
- package/src/stateful/stateful-provider-observability.ts +261 -0
- package/src/stateful/stateful-provider-owner-forwarder.ts +287 -0
- package/src/stateful/stateful-provider-runtime-context.ts +92 -0
- package/src/stateful/stateful-provider-runtime-executor.ts +96 -0
- package/src/stateful/stateful-provider-session-routing.ts +546 -0
- package/src/stateful/stateful-provider-session-runtime.ts +403 -0
- package/src/stateful-signing.ts +46 -0
- package/src/stealth/profiles.ts +27 -33
- package/src/stream-evidence.ts +988 -0
- package/src/stream.ts +16 -20
- package/src/testing/index.ts +11 -2
- package/src/testing/run.ts +668 -74
- package/src/types.ts +665 -35
- package/src/user-input.ts +118 -0
- package/dist/cli/templates/provider/CLAUDE.md.tpl +0 -1
- package/src/cli/templates/provider/CLAUDE.md.tpl +0 -1
- /package/dist/cli/templates/provider/{skills → .agents/skills}/fixtures-and-recording/SKILL.md.tpl +0 -0
- /package/dist/cli/templates/provider/{skills → .agents/skills}/health-checks-and-fail-closed/SKILL.md.tpl +0 -0
- /package/dist/cli/templates/provider/{skills → .agents/skills}/normalization-standards/SKILL.md.tpl +0 -0
- /package/dist/cli/templates/provider/{skills → .agents/skills}/pagination-and-counts/SKILL.md.tpl +0 -0
- /package/dist/cli/templates/provider/{skills → .agents/skills}/upstream-contract-verification/SKILL.md.tpl +0 -0
- /package/dist/cli/templates/provider/{skills → .agents/skills}/upstream-notes/README.md.tpl +0 -0
- /package/src/cli/templates/provider/{skills → .agents/skills}/fixtures-and-recording/SKILL.md.tpl +0 -0
- /package/src/cli/templates/provider/{skills → .agents/skills}/health-checks-and-fail-closed/SKILL.md.tpl +0 -0
- /package/src/cli/templates/provider/{skills → .agents/skills}/normalization-standards/SKILL.md.tpl +0 -0
- /package/src/cli/templates/provider/{skills → .agents/skills}/pagination-and-counts/SKILL.md.tpl +0 -0
- /package/src/cli/templates/provider/{skills → .agents/skills}/upstream-contract-verification/SKILL.md.tpl +0 -0
- /package/src/cli/templates/provider/{skills → .agents/skills}/upstream-notes/README.md.tpl +0 -0
package/src/runtime/choice.ts
CHANGED
|
@@ -4,32 +4,49 @@ 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";
|
|
42
|
+
export const PROVIDER_RUNTIME_CHOICE_WORD_ISSUANCE_ENV =
|
|
43
|
+
"APIFUSE__PROVIDER_RUNTIME__CHOICE_WORD_ISSUANCE";
|
|
30
44
|
|
|
31
45
|
const PRIMARY_CHOICE_TOKEN_KID = "v1";
|
|
32
46
|
const MANAGED_CHOICE_TOKEN_VERSION = 1;
|
|
47
|
+
const SERVER_STORED_CHOICE_RECORD_VERSION = 1;
|
|
48
|
+
const SERVER_STORED_CHOICE_ISSUE_ATTEMPTS = 5;
|
|
49
|
+
const WORD_CHOICE_NOT_FOUND_MESSAGE = "Provider choice token was not found.";
|
|
33
50
|
|
|
34
51
|
type ManagedChoiceEnvelope = {
|
|
35
52
|
readonly v: typeof MANAGED_CHOICE_TOKEN_VERSION;
|
|
@@ -51,6 +68,32 @@ type ServerChoiceHandlePayload = {
|
|
|
51
68
|
readonly created_at_ms: number;
|
|
52
69
|
};
|
|
53
70
|
|
|
71
|
+
type ServerStoredChoiceRecord = {
|
|
72
|
+
readonly v: typeof SERVER_STORED_CHOICE_RECORD_VERSION;
|
|
73
|
+
readonly storage: "server";
|
|
74
|
+
readonly status: "active" | "consumed";
|
|
75
|
+
readonly provider_id: string;
|
|
76
|
+
readonly purpose: string;
|
|
77
|
+
readonly issued_at_ms: number;
|
|
78
|
+
readonly ttl_ms: number;
|
|
79
|
+
readonly binding?: ManagedChoiceEnvelope["binding"];
|
|
80
|
+
readonly prefix: string;
|
|
81
|
+
readonly payload: ProviderChoiceTokenPayload;
|
|
82
|
+
readonly payload_digest: string;
|
|
83
|
+
readonly replay_key: string;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
export type ProviderChoiceTelemetryEvent = {
|
|
87
|
+
readonly providerId: string;
|
|
88
|
+
readonly purpose: string;
|
|
89
|
+
readonly operation: "parse" | "consume";
|
|
90
|
+
readonly format: "word" | "legacy";
|
|
91
|
+
readonly outcome: "success" | "not-found" | "invalid" | "unsupported" | "error";
|
|
92
|
+
readonly consumeMode: ProviderChoiceConsumeMode;
|
|
93
|
+
readonly consumed: boolean;
|
|
94
|
+
readonly replay: boolean;
|
|
95
|
+
};
|
|
96
|
+
|
|
54
97
|
export type CreateProviderChoiceContextOptions = {
|
|
55
98
|
readonly providerId: string;
|
|
56
99
|
readonly env?: EnvContext;
|
|
@@ -59,6 +102,8 @@ export type CreateProviderChoiceContextOptions = {
|
|
|
59
102
|
readonly state?: ProviderRuntimeState;
|
|
60
103
|
readonly masterSecret?: string;
|
|
61
104
|
readonly kid?: string;
|
|
105
|
+
/** Receives allowlisted metadata only; token and payload values are never included. */
|
|
106
|
+
readonly onTelemetry?: (event: ProviderChoiceTelemetryEvent) => void;
|
|
62
107
|
};
|
|
63
108
|
|
|
64
109
|
export function createProviderChoiceContext(
|
|
@@ -74,24 +119,74 @@ export function createProviderChoiceContext(
|
|
|
74
119
|
): string;
|
|
75
120
|
function issue<TPayload extends ProviderChoiceTokenPayload>(
|
|
76
121
|
issueOptions: ProviderChoiceIssueOptions<TPayload> & {
|
|
77
|
-
readonly storage: Extract<
|
|
78
|
-
ProviderChoiceStorageOptions,
|
|
79
|
-
{ readonly mode: "server" }
|
|
80
|
-
>;
|
|
122
|
+
readonly storage: Extract<ProviderChoiceStorageOptions, { readonly mode: "server" }>;
|
|
81
123
|
},
|
|
82
124
|
): Promise<string>;
|
|
83
125
|
function issue<TPayload extends ProviderChoiceTokenPayload>(
|
|
84
126
|
issueOptions: ProviderChoiceIssueOptions<TPayload> & {
|
|
85
|
-
readonly storage: Extract<
|
|
86
|
-
ProviderChoiceStorageOptions,
|
|
87
|
-
{ readonly mode: "auto" }
|
|
88
|
-
>;
|
|
127
|
+
readonly storage: Extract<ProviderChoiceStorageOptions, { readonly mode: "auto" }>;
|
|
89
128
|
},
|
|
90
129
|
): string | Promise<string>;
|
|
91
130
|
function issue<TPayload extends ProviderChoiceTokenPayload>(
|
|
92
131
|
issueOptions: ProviderChoiceIssueOptions<TPayload>,
|
|
93
132
|
): string | Promise<string> {
|
|
94
133
|
const issuedAtMs = issueOptions.nowMs ?? Date.now();
|
|
134
|
+
const resolvedStorage = resolveIssueStorage(issueOptions.storage, issueOptions.payload);
|
|
135
|
+
if (resolvedStorage.mode === "server") {
|
|
136
|
+
const issuance = resolveChoiceWordIssuance(options.env);
|
|
137
|
+
const keys = hasRequestedChoiceBinding(issueOptions.bind)
|
|
138
|
+
? deriveManagedChoiceKeys({
|
|
139
|
+
masterSecret: resolveMasterSecret(),
|
|
140
|
+
providerId: options.providerId,
|
|
141
|
+
purpose: issueOptions.purpose,
|
|
142
|
+
kid,
|
|
143
|
+
})
|
|
144
|
+
: undefined;
|
|
145
|
+
const binding = hasRequestedChoiceBinding(issueOptions.bind)
|
|
146
|
+
? createChoiceBinding({
|
|
147
|
+
keys: keys!,
|
|
148
|
+
options: issueOptions.bind,
|
|
149
|
+
request: options.request,
|
|
150
|
+
credential: options.credential,
|
|
151
|
+
required: true,
|
|
152
|
+
})
|
|
153
|
+
: undefined;
|
|
154
|
+
const baseEnvelope = {
|
|
155
|
+
v: MANAGED_CHOICE_TOKEN_VERSION,
|
|
156
|
+
provider_id: options.providerId,
|
|
157
|
+
purpose: issueOptions.purpose,
|
|
158
|
+
issued_at_ms: issuedAtMs,
|
|
159
|
+
ttl_ms: issueOptions.ttlMs,
|
|
160
|
+
binding,
|
|
161
|
+
} satisfies Omit<ManagedChoiceEnvelope, "payload">;
|
|
162
|
+
if (issuance === "legacy") {
|
|
163
|
+
const legacyKeys =
|
|
164
|
+
keys ??
|
|
165
|
+
deriveManagedChoiceKeys({
|
|
166
|
+
masterSecret: resolveMasterSecret(),
|
|
167
|
+
providerId: options.providerId,
|
|
168
|
+
purpose: issueOptions.purpose,
|
|
169
|
+
kid,
|
|
170
|
+
});
|
|
171
|
+
return issueLegacyServerStoredChoice({
|
|
172
|
+
baseEnvelope,
|
|
173
|
+
issueOptions,
|
|
174
|
+
storage: resolvedStorage.storage,
|
|
175
|
+
contextState: options.state,
|
|
176
|
+
kid,
|
|
177
|
+
keys: legacyKeys,
|
|
178
|
+
issuedAtMs,
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
return issueWordServerStoredChoice({
|
|
182
|
+
baseEnvelope: {
|
|
183
|
+
...baseEnvelope,
|
|
184
|
+
},
|
|
185
|
+
issueOptions,
|
|
186
|
+
storage: resolvedStorage.storage,
|
|
187
|
+
contextState: options.state,
|
|
188
|
+
});
|
|
189
|
+
}
|
|
95
190
|
const keys = deriveManagedChoiceKeys({
|
|
96
191
|
masterSecret: resolveMasterSecret(),
|
|
97
192
|
providerId: options.providerId,
|
|
@@ -112,21 +207,6 @@ export function createProviderChoiceContext(
|
|
|
112
207
|
required: true,
|
|
113
208
|
}),
|
|
114
209
|
};
|
|
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
210
|
const envelope: ManagedChoiceEnvelope = {
|
|
131
211
|
...baseEnvelope,
|
|
132
212
|
payload: issueOptions.payload,
|
|
@@ -139,6 +219,9 @@ export function createProviderChoiceContext(
|
|
|
139
219
|
});
|
|
140
220
|
}
|
|
141
221
|
|
|
222
|
+
function parse(
|
|
223
|
+
parseOptions: ProviderChoiceParseOptions & { readonly consume: "explicit" },
|
|
224
|
+
): Promise<ProviderChoiceExplicitParseResult>;
|
|
142
225
|
function parse(
|
|
143
226
|
parseOptions: ProviderChoiceParseOptions & {
|
|
144
227
|
readonly storage?: { readonly mode: "inline" };
|
|
@@ -146,94 +229,168 @@ export function createProviderChoiceContext(
|
|
|
146
229
|
): ProviderChoiceTokenPayload;
|
|
147
230
|
function parse(
|
|
148
231
|
parseOptions: ProviderChoiceParseOptions & {
|
|
149
|
-
readonly storage: Extract<
|
|
150
|
-
ProviderChoiceStorageOptions,
|
|
151
|
-
{ readonly mode: "server" }
|
|
152
|
-
>;
|
|
232
|
+
readonly storage: Extract<ProviderChoiceStorageOptions, { readonly mode: "server" }>;
|
|
153
233
|
},
|
|
154
234
|
): Promise<ProviderChoiceTokenPayload>;
|
|
155
235
|
function parse(
|
|
156
236
|
parseOptions: ProviderChoiceParseOptions & {
|
|
157
|
-
readonly storage: Extract<
|
|
158
|
-
ProviderChoiceStorageOptions,
|
|
159
|
-
{ readonly mode: "auto" }
|
|
160
|
-
>;
|
|
237
|
+
readonly storage: Extract<ProviderChoiceStorageOptions, { readonly mode: "auto" }>;
|
|
161
238
|
},
|
|
162
239
|
): ProviderChoiceTokenPayload | Promise<ProviderChoiceTokenPayload>;
|
|
163
240
|
function parse(
|
|
164
241
|
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,
|
|
242
|
+
):
|
|
243
|
+
| ProviderChoiceTokenPayload
|
|
244
|
+
| ProviderChoiceExplicitParseResult
|
|
245
|
+
| Promise<ProviderChoiceTokenPayload | ProviderChoiceExplicitParseResult> {
|
|
246
|
+
const consumeMode = parseOptions.consume ?? "never";
|
|
247
|
+
const wordStateKey = parseWordChoiceStateKey({
|
|
248
|
+
token: parseOptions.token,
|
|
249
|
+
prefix: parseOptions.prefix,
|
|
218
250
|
});
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
251
|
+
if (wordStateKey) {
|
|
252
|
+
const parsed = parseWordServerStoredChoice({
|
|
253
|
+
stateKey: wordStateKey,
|
|
254
|
+
parseOptions,
|
|
255
|
+
contextState: options.state,
|
|
256
|
+
providerId: options.providerId,
|
|
224
257
|
request: options.request,
|
|
225
258
|
credential: options.credential,
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
259
|
+
resolveBindingKeys: () =>
|
|
260
|
+
deriveManagedChoiceKeys({
|
|
261
|
+
masterSecret: resolveMasterSecret(),
|
|
262
|
+
providerId: options.providerId,
|
|
263
|
+
purpose: parseOptions.purpose,
|
|
264
|
+
kid,
|
|
265
|
+
}),
|
|
266
|
+
onConsume: (result) =>
|
|
267
|
+
emitChoiceTelemetry(options.onTelemetry, {
|
|
268
|
+
providerId: options.providerId,
|
|
269
|
+
purpose: parseOptions.purpose,
|
|
270
|
+
operation: "consume",
|
|
271
|
+
format: "word",
|
|
272
|
+
outcome: "success",
|
|
273
|
+
consumeMode,
|
|
274
|
+
consumed: result.status === "consumed",
|
|
275
|
+
replay: result.status === "already-consumed",
|
|
276
|
+
}),
|
|
277
|
+
});
|
|
278
|
+
return observeChoiceParse<ProviderChoiceTokenPayload | ProviderChoiceExplicitParseResult>(
|
|
279
|
+
parsed,
|
|
280
|
+
{
|
|
281
|
+
onTelemetry: options.onTelemetry,
|
|
282
|
+
providerId: options.providerId,
|
|
283
|
+
purpose: parseOptions.purpose,
|
|
284
|
+
format: "word",
|
|
285
|
+
consumeMode,
|
|
286
|
+
},
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Legacy encrypted-envelope compatibility fallback. Removal is gated on
|
|
291
|
+
// the last legacy mint plus the maximum issued TTL; see ADR 0006.
|
|
292
|
+
// A structurally valid word token returns above, so lookup, expiry,
|
|
293
|
+
// consumption, and binding failures can never enter this branch.
|
|
294
|
+
try {
|
|
295
|
+
const [actualPrefix, tokenKid, encodedIv, encryptedPayload, authTag, signature] =
|
|
296
|
+
parseManagedChoiceTokenParts(parseOptions.token);
|
|
297
|
+
if (
|
|
298
|
+
actualPrefix !== parseOptions.prefix ||
|
|
299
|
+
tokenKid !== kid ||
|
|
300
|
+
!encodedIv ||
|
|
301
|
+
!encryptedPayload ||
|
|
302
|
+
!authTag ||
|
|
303
|
+
!signature
|
|
304
|
+
) {
|
|
305
|
+
throw new ProviderChoiceTokenError(
|
|
306
|
+
"invalid_shape",
|
|
307
|
+
"Provider choice token shape is invalid.",
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const keys = deriveManagedChoiceKeys({
|
|
312
|
+
masterSecret: resolveMasterSecret(),
|
|
313
|
+
providerId: options.providerId,
|
|
314
|
+
purpose: parseOptions.purpose,
|
|
315
|
+
kid: tokenKid,
|
|
316
|
+
});
|
|
317
|
+
const signedBody = [parseOptions.prefix, tokenKid, encodedIv, encryptedPayload, authTag].join(
|
|
318
|
+
".",
|
|
319
|
+
);
|
|
320
|
+
assertManagedChoiceSignature({
|
|
321
|
+
signedBody,
|
|
322
|
+
signature,
|
|
323
|
+
signingKey: keys.signing,
|
|
324
|
+
});
|
|
325
|
+
const envelope = decryptManagedChoiceToken({
|
|
326
|
+
encodedIv,
|
|
327
|
+
encryptedPayload,
|
|
328
|
+
authTag,
|
|
329
|
+
encryptionKey: keys.encryption,
|
|
330
|
+
});
|
|
331
|
+
assertManagedChoiceEnvelope(envelope, {
|
|
332
|
+
providerId: options.providerId,
|
|
333
|
+
purpose: parseOptions.purpose,
|
|
334
|
+
ttlMs: parseOptions.ttlMs,
|
|
335
|
+
nowMs: parseOptions.nowMs,
|
|
336
|
+
futureToleranceMs: parseOptions.futureToleranceMs,
|
|
234
337
|
});
|
|
338
|
+
assertChoiceBindingMatches({
|
|
339
|
+
actual: envelope.binding,
|
|
340
|
+
expected: createChoiceBinding({
|
|
341
|
+
keys,
|
|
342
|
+
options: parseOptions.bind,
|
|
343
|
+
request: options.request,
|
|
344
|
+
credential: options.credential,
|
|
345
|
+
required: true,
|
|
346
|
+
}),
|
|
347
|
+
});
|
|
348
|
+
const payload = isServerChoiceHandlePayload(envelope.payload)
|
|
349
|
+
? parseLegacyServerStoredChoice({
|
|
350
|
+
handle: envelope.payload,
|
|
351
|
+
storage: parseOptions.storage,
|
|
352
|
+
contextState: options.state,
|
|
353
|
+
})
|
|
354
|
+
: envelope.payload;
|
|
355
|
+
const parsed =
|
|
356
|
+
consumeMode === "explicit"
|
|
357
|
+
? Promise.resolve(payload).then((resolvedPayload) =>
|
|
358
|
+
createLegacyExplicitParseResult({
|
|
359
|
+
payload: resolvedPayload,
|
|
360
|
+
replayKey: digestChoiceReplayKey(parseOptions.token),
|
|
361
|
+
onConsume: () =>
|
|
362
|
+
emitChoiceTelemetry(options.onTelemetry, {
|
|
363
|
+
providerId: options.providerId,
|
|
364
|
+
purpose: parseOptions.purpose,
|
|
365
|
+
operation: "consume",
|
|
366
|
+
format: "legacy",
|
|
367
|
+
outcome: "unsupported",
|
|
368
|
+
consumeMode,
|
|
369
|
+
consumed: false,
|
|
370
|
+
replay: false,
|
|
371
|
+
}),
|
|
372
|
+
}),
|
|
373
|
+
)
|
|
374
|
+
: payload;
|
|
375
|
+
return observeChoiceParse<ProviderChoiceTokenPayload | ProviderChoiceExplicitParseResult>(
|
|
376
|
+
parsed,
|
|
377
|
+
{
|
|
378
|
+
onTelemetry: options.onTelemetry,
|
|
379
|
+
providerId: options.providerId,
|
|
380
|
+
purpose: parseOptions.purpose,
|
|
381
|
+
format: "legacy",
|
|
382
|
+
consumeMode,
|
|
383
|
+
},
|
|
384
|
+
);
|
|
385
|
+
} catch (error) {
|
|
386
|
+
emitChoiceParseFailure(options.onTelemetry, error, {
|
|
387
|
+
providerId: options.providerId,
|
|
388
|
+
purpose: parseOptions.purpose,
|
|
389
|
+
format: "legacy",
|
|
390
|
+
consumeMode,
|
|
391
|
+
});
|
|
392
|
+
throw error;
|
|
235
393
|
}
|
|
236
|
-
return envelope.payload;
|
|
237
394
|
}
|
|
238
395
|
|
|
239
396
|
return { issue, parse };
|
|
@@ -247,32 +404,144 @@ export function createTestProviderChoiceContext(
|
|
|
247
404
|
return createProviderChoiceContext({
|
|
248
405
|
...options,
|
|
249
406
|
masterSecret:
|
|
250
|
-
options.masterSecret ??
|
|
251
|
-
"apifuse-test-provider-runtime-choice-token-master-secret",
|
|
407
|
+
options.masterSecret ?? "apifuse-test-provider-runtime-choice-token-master-secret",
|
|
252
408
|
});
|
|
253
409
|
}
|
|
254
410
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
411
|
+
type ChoiceParseTelemetryBase = {
|
|
412
|
+
readonly onTelemetry?: (event: ProviderChoiceTelemetryEvent) => void;
|
|
413
|
+
readonly providerId: string;
|
|
414
|
+
readonly purpose: string;
|
|
415
|
+
readonly format: "word" | "legacy";
|
|
416
|
+
readonly consumeMode: ProviderChoiceConsumeMode;
|
|
417
|
+
};
|
|
418
|
+
|
|
419
|
+
function observeChoiceParse<T>(
|
|
420
|
+
result: T | Promise<T>,
|
|
421
|
+
base: ChoiceParseTelemetryBase,
|
|
422
|
+
): T | Promise<T> {
|
|
423
|
+
if (result instanceof Promise) {
|
|
424
|
+
return result.then(
|
|
425
|
+
(value) => {
|
|
426
|
+
emitChoiceParseSuccess(base, value);
|
|
427
|
+
return value;
|
|
428
|
+
},
|
|
429
|
+
(error: unknown) => {
|
|
430
|
+
emitChoiceParseFailure(base.onTelemetry, error, base);
|
|
431
|
+
throw error;
|
|
432
|
+
},
|
|
433
|
+
);
|
|
434
|
+
}
|
|
435
|
+
emitChoiceParseSuccess(base, result);
|
|
436
|
+
return result;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
function emitChoiceParseSuccess(base: ChoiceParseTelemetryBase, result: unknown): void {
|
|
440
|
+
const replay = isConsumedChoiceReplay(result);
|
|
441
|
+
emitChoiceTelemetry(base.onTelemetry, {
|
|
442
|
+
providerId: base.providerId,
|
|
443
|
+
purpose: base.purpose,
|
|
444
|
+
operation: "parse",
|
|
445
|
+
format: base.format,
|
|
446
|
+
outcome: "success",
|
|
447
|
+
consumeMode: base.consumeMode,
|
|
448
|
+
consumed: replay || (base.format === "word" && base.consumeMode === "on-parse"),
|
|
449
|
+
replay,
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
function isConsumedChoiceReplay(value: unknown): boolean {
|
|
454
|
+
return (
|
|
455
|
+
value !== null &&
|
|
456
|
+
typeof value === "object" &&
|
|
457
|
+
"status" in value &&
|
|
458
|
+
value.status === "consumed" &&
|
|
459
|
+
"replayKey" in value &&
|
|
460
|
+
typeof value.replayKey === "string"
|
|
461
|
+
);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
function emitChoiceParseFailure(
|
|
465
|
+
onTelemetry: CreateProviderChoiceContextOptions["onTelemetry"],
|
|
466
|
+
error: unknown,
|
|
467
|
+
base: Omit<ChoiceParseTelemetryBase, "onTelemetry">,
|
|
468
|
+
): void {
|
|
469
|
+
const outcome =
|
|
470
|
+
error instanceof ProviderChoiceTokenError
|
|
471
|
+
? base.format === "word" && error.message === WORD_CHOICE_NOT_FOUND_MESSAGE
|
|
472
|
+
? "not-found"
|
|
473
|
+
: "invalid"
|
|
474
|
+
: "error";
|
|
475
|
+
emitChoiceTelemetry(onTelemetry, {
|
|
476
|
+
providerId: base.providerId,
|
|
477
|
+
purpose: base.purpose,
|
|
478
|
+
operation: "parse",
|
|
479
|
+
format: base.format,
|
|
480
|
+
outcome,
|
|
481
|
+
consumeMode: base.consumeMode,
|
|
482
|
+
consumed: false,
|
|
483
|
+
replay: false,
|
|
484
|
+
});
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
function emitChoiceTelemetry(
|
|
488
|
+
onTelemetry: CreateProviderChoiceContextOptions["onTelemetry"],
|
|
489
|
+
event: ProviderChoiceTelemetryEvent,
|
|
490
|
+
): void {
|
|
491
|
+
try {
|
|
492
|
+
onTelemetry?.(event);
|
|
493
|
+
} catch {
|
|
494
|
+
// Observability must never change provider token semantics.
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
function createLegacyExplicitParseResult(options: {
|
|
499
|
+
readonly payload: ProviderChoiceTokenPayload;
|
|
500
|
+
readonly replayKey: string;
|
|
501
|
+
readonly onConsume: () => void;
|
|
502
|
+
}): ProviderChoiceExplicitParseResult {
|
|
503
|
+
return {
|
|
504
|
+
status: "active",
|
|
505
|
+
payload: options.payload,
|
|
506
|
+
replayKey: options.replayKey,
|
|
507
|
+
consume: async () => {
|
|
508
|
+
options.onConsume();
|
|
509
|
+
return { status: "unsupported" };
|
|
510
|
+
},
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
function resolveChoiceWordIssuance(env?: EnvContext): "legacy" | "word" {
|
|
515
|
+
const configured = env?.get(PROVIDER_RUNTIME_CHOICE_WORD_ISSUANCE_ENV);
|
|
516
|
+
const value = configured?.trim() ?? "";
|
|
517
|
+
if (value === "" || value === "legacy") return "legacy";
|
|
518
|
+
if (value === "word") return "word";
|
|
263
519
|
throw new ProviderError(
|
|
264
|
-
|
|
520
|
+
`Unsupported provider choice word issuance mode "${value}". Expected "legacy" or "word".`,
|
|
265
521
|
{
|
|
266
|
-
code: "
|
|
267
|
-
category: "
|
|
522
|
+
code: "CHOICE_WORD_ISSUANCE_INVALID",
|
|
523
|
+
category: "input_validation",
|
|
268
524
|
retryable: false,
|
|
269
|
-
details: {
|
|
270
|
-
secret: PROVIDER_RUNTIME_CHOICE_TOKEN_MASTER_SECRET_ENV,
|
|
271
|
-
},
|
|
525
|
+
details: { env: PROVIDER_RUNTIME_CHOICE_WORD_ISSUANCE_ENV },
|
|
272
526
|
},
|
|
273
527
|
);
|
|
274
528
|
}
|
|
275
529
|
|
|
530
|
+
function resolveChoiceMasterSecret(options: CreateProviderChoiceContextOptions): string {
|
|
531
|
+
const configured =
|
|
532
|
+
options.masterSecret ?? options.env?.get(PROVIDER_RUNTIME_CHOICE_TOKEN_MASTER_SECRET_ENV);
|
|
533
|
+
const trimmed = configured?.trim();
|
|
534
|
+
if (trimmed) return trimmed;
|
|
535
|
+
throw new ProviderError("Provider runtime choice-token master secret is not configured.", {
|
|
536
|
+
code: "CHOICE_TOKEN_MASTER_SECRET_NOT_CONFIGURED",
|
|
537
|
+
category: "internal_error",
|
|
538
|
+
retryable: false,
|
|
539
|
+
details: {
|
|
540
|
+
secret: PROVIDER_RUNTIME_CHOICE_TOKEN_MASTER_SECRET_ENV,
|
|
541
|
+
},
|
|
542
|
+
});
|
|
543
|
+
}
|
|
544
|
+
|
|
276
545
|
type ManagedChoiceKeyInput = {
|
|
277
546
|
readonly masterSecret: string;
|
|
278
547
|
readonly providerId: string;
|
|
@@ -286,9 +555,7 @@ type ManagedChoiceKeys = {
|
|
|
286
555
|
readonly binding: Buffer;
|
|
287
556
|
};
|
|
288
557
|
|
|
289
|
-
function deriveManagedChoiceKeys(
|
|
290
|
-
input: ManagedChoiceKeyInput,
|
|
291
|
-
): ManagedChoiceKeys {
|
|
558
|
+
function deriveManagedChoiceKeys(input: ManagedChoiceKeyInput): ManagedChoiceKeys {
|
|
292
559
|
return {
|
|
293
560
|
encryption: deriveManagedChoiceKey(input, "encryption"),
|
|
294
561
|
signing: deriveManagedChoiceKey(input, "signing"),
|
|
@@ -327,50 +594,94 @@ function encryptManagedChoiceToken(options: {
|
|
|
327
594
|
]).toString("base64url");
|
|
328
595
|
const authTag = cipher.getAuthTag().toString("base64url");
|
|
329
596
|
const encodedIv = iv.toString("base64url");
|
|
330
|
-
const signedBody = [
|
|
331
|
-
options.prefix,
|
|
332
|
-
options.kid,
|
|
333
|
-
encodedIv,
|
|
334
|
-
encryptedPayload,
|
|
335
|
-
authTag,
|
|
336
|
-
].join(".");
|
|
597
|
+
const signedBody = [options.prefix, options.kid, encodedIv, encryptedPayload, authTag].join(".");
|
|
337
598
|
const signature = createHmac("sha256", options.keys.signing)
|
|
338
599
|
.update(signedBody)
|
|
339
600
|
.digest("base64url");
|
|
340
601
|
return `${signedBody}.${signature}`;
|
|
341
602
|
}
|
|
342
603
|
|
|
343
|
-
async function
|
|
344
|
-
TPayload extends ProviderChoiceTokenPayload,
|
|
345
|
-
>(options: {
|
|
604
|
+
async function issueWordServerStoredChoice<TPayload extends ProviderChoiceTokenPayload>(options: {
|
|
346
605
|
readonly baseEnvelope: Omit<ManagedChoiceEnvelope, "payload">;
|
|
347
606
|
readonly issueOptions: ProviderChoiceIssueOptions<TPayload>;
|
|
348
607
|
readonly storage: ServerProviderChoiceStorageOptions;
|
|
349
608
|
readonly contextState?: ProviderRuntimeState;
|
|
350
|
-
readonly kid: string;
|
|
351
|
-
readonly keys: ManagedChoiceKeys;
|
|
352
|
-
readonly issuedAtMs: number;
|
|
353
609
|
}): Promise<string> {
|
|
354
|
-
const serializedPayload = serializeChoicePayload(
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
610
|
+
const serializedPayload = serializeChoicePayload(options.issueOptions.payload);
|
|
611
|
+
const payloadDigest = digestChoicePayload(serializedPayload);
|
|
612
|
+
const namespace = resolveChoiceStateNamespace({
|
|
613
|
+
storage: options.storage,
|
|
614
|
+
contextState: options.contextState,
|
|
615
|
+
ttlMs: options.issueOptions.ttlMs,
|
|
616
|
+
});
|
|
617
|
+
const wordCount =
|
|
618
|
+
options.issueOptions.strength === "high" ? HIGH_CHOICE_WORD_COUNT : STANDARD_CHOICE_WORD_COUNT;
|
|
619
|
+
for (let attempt = 0; attempt < SERVER_STORED_CHOICE_ISSUE_ATTEMPTS; attempt += 1) {
|
|
620
|
+
const stateKey = generateChoiceWordSequence(wordCount);
|
|
621
|
+
const token = `${options.issueOptions.prefix}${stateKey}`;
|
|
622
|
+
const record: ServerStoredChoiceRecord = {
|
|
623
|
+
v: SERVER_STORED_CHOICE_RECORD_VERSION,
|
|
624
|
+
storage: "server",
|
|
625
|
+
status: "active",
|
|
626
|
+
provider_id: options.baseEnvelope.provider_id,
|
|
627
|
+
purpose: options.baseEnvelope.purpose,
|
|
628
|
+
issued_at_ms: options.baseEnvelope.issued_at_ms,
|
|
629
|
+
ttl_ms: options.baseEnvelope.ttl_ms,
|
|
630
|
+
binding: options.baseEnvelope.binding,
|
|
631
|
+
prefix: options.issueOptions.prefix,
|
|
632
|
+
payload: options.issueOptions.payload,
|
|
633
|
+
payload_digest: payloadDigest,
|
|
634
|
+
replay_key: digestChoiceReplayKey(token),
|
|
635
|
+
};
|
|
636
|
+
const valueBytes = Buffer.byteLength(JSON.stringify(record), "utf8");
|
|
637
|
+
if (valueBytes > options.storage.maxValueBytes) {
|
|
638
|
+
throw new ProviderError("Provider choice payload exceeds state storage policy.", {
|
|
362
639
|
code: "CHOICE_STATE_PAYLOAD_TOO_LARGE",
|
|
363
640
|
category: "input_validation",
|
|
364
641
|
retryable: false,
|
|
365
642
|
details: {
|
|
366
643
|
maxValueBytes: options.storage.maxValueBytes,
|
|
367
|
-
|
|
644
|
+
valueBytes,
|
|
368
645
|
},
|
|
369
|
-
}
|
|
370
|
-
|
|
646
|
+
});
|
|
647
|
+
}
|
|
648
|
+
const result = await namespace.compareAndSet(optionsStateKey(stateKey), 0, record, {
|
|
649
|
+
ttl: stateTtl(options.storage, options.issueOptions.ttlMs),
|
|
650
|
+
});
|
|
651
|
+
if (result.ok) return token;
|
|
652
|
+
}
|
|
653
|
+
throw new ProviderError("Provider choice state storage is not available.", {
|
|
654
|
+
code: "CHOICE_STATE_UNAVAILABLE",
|
|
655
|
+
category: "internal_error",
|
|
656
|
+
retryable: false,
|
|
657
|
+
});
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
/**
|
|
661
|
+
* Beta.28-compatible server handle issuance. The state stores the payload and
|
|
662
|
+
* the client receives the existing six-part encrypted envelope whose payload
|
|
663
|
+
* is a server-state handle.
|
|
664
|
+
*/
|
|
665
|
+
async function issueLegacyServerStoredChoice<TPayload extends ProviderChoiceTokenPayload>(options: {
|
|
666
|
+
readonly baseEnvelope: Omit<ManagedChoiceEnvelope, "payload">;
|
|
667
|
+
readonly issueOptions: ProviderChoiceIssueOptions<TPayload>;
|
|
668
|
+
readonly storage: ServerProviderChoiceStorageOptions;
|
|
669
|
+
readonly contextState?: ProviderRuntimeState;
|
|
670
|
+
readonly kid: string;
|
|
671
|
+
readonly keys: ManagedChoiceKeys;
|
|
672
|
+
readonly issuedAtMs: number;
|
|
673
|
+
}): Promise<string> {
|
|
674
|
+
const serializedPayload = serializeChoicePayload(options.issueOptions.payload);
|
|
675
|
+
const payloadBytes = Buffer.byteLength(serializedPayload, "utf8");
|
|
676
|
+
if (payloadBytes > options.storage.maxValueBytes) {
|
|
677
|
+
throw new ProviderError("Provider choice payload exceeds state storage policy.", {
|
|
678
|
+
code: "CHOICE_STATE_PAYLOAD_TOO_LARGE",
|
|
679
|
+
category: "input_validation",
|
|
680
|
+
retryable: false,
|
|
681
|
+
details: { maxValueBytes: options.storage.maxValueBytes, payloadBytes },
|
|
682
|
+
});
|
|
371
683
|
}
|
|
372
684
|
const stateId = `choice_${randomBytes(16).toString("base64url")}`;
|
|
373
|
-
const digest = digestChoicePayload(serializedPayload);
|
|
374
685
|
const namespace = resolveChoiceStateNamespace({
|
|
375
686
|
storage: options.storage,
|
|
376
687
|
contextState: options.contextState,
|
|
@@ -384,7 +695,7 @@ async function issueServerStoredChoice<
|
|
|
384
695
|
payload: {
|
|
385
696
|
storage: "server",
|
|
386
697
|
state_id: stateId,
|
|
387
|
-
payload_digest:
|
|
698
|
+
payload_digest: digestChoicePayload(serializedPayload),
|
|
388
699
|
created_at_ms: options.issuedAtMs,
|
|
389
700
|
},
|
|
390
701
|
};
|
|
@@ -396,7 +707,149 @@ async function issueServerStoredChoice<
|
|
|
396
707
|
});
|
|
397
708
|
}
|
|
398
709
|
|
|
399
|
-
async function
|
|
710
|
+
async function parseWordServerStoredChoice(options: {
|
|
711
|
+
readonly stateKey: string;
|
|
712
|
+
readonly parseOptions: ProviderChoiceParseOptions;
|
|
713
|
+
readonly contextState?: ProviderRuntimeState;
|
|
714
|
+
readonly providerId: string;
|
|
715
|
+
readonly request?: ProviderRequestContext;
|
|
716
|
+
readonly credential?: CredentialContext;
|
|
717
|
+
readonly resolveBindingKeys: () => ManagedChoiceKeys;
|
|
718
|
+
readonly onConsume: (result: ProviderChoiceConsumeResult) => void;
|
|
719
|
+
}): Promise<ProviderChoiceTokenPayload | ProviderChoiceExplicitParseResult> {
|
|
720
|
+
const storage = resolveParseStorage(options.parseOptions.storage);
|
|
721
|
+
const namespace = resolveChoiceStateNamespace({
|
|
722
|
+
storage,
|
|
723
|
+
contextState: options.contextState,
|
|
724
|
+
ttlMs: options.parseOptions.ttlMs,
|
|
725
|
+
});
|
|
726
|
+
let stored: StateValue<ServerStoredChoiceRecord> | null;
|
|
727
|
+
try {
|
|
728
|
+
stored = await namespace.get<ServerStoredChoiceRecord>(optionsStateKey(options.stateKey));
|
|
729
|
+
} catch (error) {
|
|
730
|
+
if (isProviderError(error)) throw error;
|
|
731
|
+
throw wordChoiceNotFoundError();
|
|
732
|
+
}
|
|
733
|
+
if (!stored || !isServerStoredChoiceRecord(stored.value)) {
|
|
734
|
+
throw wordChoiceNotFoundError();
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
const record = stored.value;
|
|
738
|
+
const expectedReplayKey = digestChoiceReplayKey(
|
|
739
|
+
`${options.parseOptions.prefix}${options.stateKey}`,
|
|
740
|
+
);
|
|
741
|
+
try {
|
|
742
|
+
if (
|
|
743
|
+
record.provider_id !== options.providerId ||
|
|
744
|
+
record.purpose !== options.parseOptions.purpose ||
|
|
745
|
+
record.prefix !== options.parseOptions.prefix
|
|
746
|
+
) {
|
|
747
|
+
throw wordChoiceNotFoundError();
|
|
748
|
+
}
|
|
749
|
+
assertFreshProviderChoiceIssuedAt(record.issued_at_ms, {
|
|
750
|
+
ttlMs:
|
|
751
|
+
options.parseOptions.ttlMs != null
|
|
752
|
+
? Math.min(options.parseOptions.ttlMs, record.ttl_ms)
|
|
753
|
+
: record.ttl_ms,
|
|
754
|
+
nowMs: options.parseOptions.nowMs,
|
|
755
|
+
futureToleranceMs: options.parseOptions.futureToleranceMs,
|
|
756
|
+
});
|
|
757
|
+
assertPayloadDigestMatches({
|
|
758
|
+
actual: digestChoicePayload(serializeChoicePayload(record.payload)),
|
|
759
|
+
expected: record.payload_digest,
|
|
760
|
+
});
|
|
761
|
+
assertPayloadDigestMatches({ actual: expectedReplayKey, expected: record.replay_key });
|
|
762
|
+
assertWordChoiceBindingMatches({
|
|
763
|
+
actual: record.binding,
|
|
764
|
+
requested: options.parseOptions.bind,
|
|
765
|
+
request: options.request,
|
|
766
|
+
credential: options.credential,
|
|
767
|
+
resolveKeys: options.resolveBindingKeys,
|
|
768
|
+
});
|
|
769
|
+
} catch (error) {
|
|
770
|
+
if (
|
|
771
|
+
error instanceof ProviderChoiceTokenError ||
|
|
772
|
+
(isProviderError(error) && error.code === "CHOICE_CONTEXT_REQUIRED")
|
|
773
|
+
) {
|
|
774
|
+
throw wordChoiceNotFoundError();
|
|
775
|
+
}
|
|
776
|
+
throw error;
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
const consumeMode = options.parseOptions.consume ?? "never";
|
|
780
|
+
if (record.status === "consumed") {
|
|
781
|
+
if (consumeMode === "explicit") {
|
|
782
|
+
return { status: "consumed", replayKey: record.replay_key };
|
|
783
|
+
}
|
|
784
|
+
throw wordChoiceNotFoundError();
|
|
785
|
+
}
|
|
786
|
+
if (consumeMode === "never") return record.payload;
|
|
787
|
+
if (consumeMode === "explicit") {
|
|
788
|
+
return {
|
|
789
|
+
status: "active",
|
|
790
|
+
payload: record.payload,
|
|
791
|
+
replayKey: record.replay_key,
|
|
792
|
+
consume: async () => {
|
|
793
|
+
const result = await consumeWordServerStoredChoice({
|
|
794
|
+
stateKey: options.stateKey,
|
|
795
|
+
stored,
|
|
796
|
+
record,
|
|
797
|
+
storage,
|
|
798
|
+
contextState: options.contextState,
|
|
799
|
+
});
|
|
800
|
+
options.onConsume(result);
|
|
801
|
+
return result;
|
|
802
|
+
},
|
|
803
|
+
};
|
|
804
|
+
}
|
|
805
|
+
const consumed = await consumeWordServerStoredChoice({
|
|
806
|
+
stateKey: options.stateKey,
|
|
807
|
+
stored,
|
|
808
|
+
record,
|
|
809
|
+
storage,
|
|
810
|
+
contextState: options.contextState,
|
|
811
|
+
});
|
|
812
|
+
if (consumed.status !== "consumed") throw wordChoiceNotFoundError();
|
|
813
|
+
return record.payload;
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
async function consumeWordServerStoredChoice(options: {
|
|
817
|
+
readonly stateKey: string;
|
|
818
|
+
readonly stored: StateValue<ServerStoredChoiceRecord>;
|
|
819
|
+
readonly record: ServerStoredChoiceRecord;
|
|
820
|
+
readonly storage: ServerProviderChoiceStorageOptions;
|
|
821
|
+
readonly contextState?: ProviderRuntimeState;
|
|
822
|
+
}): Promise<ProviderChoiceConsumeResult> {
|
|
823
|
+
const namespace = resolveChoiceStateNamespace({
|
|
824
|
+
storage: options.storage,
|
|
825
|
+
contextState: options.contextState,
|
|
826
|
+
ttlMs: options.record.ttl_ms,
|
|
827
|
+
});
|
|
828
|
+
try {
|
|
829
|
+
const consumed = await namespace.compareAndSet(
|
|
830
|
+
optionsStateKey(options.stateKey),
|
|
831
|
+
options.stored.version,
|
|
832
|
+
{ ...options.record, status: "consumed" } satisfies ServerStoredChoiceRecord,
|
|
833
|
+
{ ttl: remainingStateTtl(options.stored.expiresAt) },
|
|
834
|
+
);
|
|
835
|
+
if (consumed.ok) return { status: "consumed" };
|
|
836
|
+
if (
|
|
837
|
+
consumed.current &&
|
|
838
|
+
isServerStoredChoiceRecord(consumed.current.value) &&
|
|
839
|
+
consumed.current.value.status === "consumed" &&
|
|
840
|
+
consumed.current.value.replay_key === options.record.replay_key
|
|
841
|
+
) {
|
|
842
|
+
return { status: "already-consumed" };
|
|
843
|
+
}
|
|
844
|
+
throw wordChoiceNotFoundError();
|
|
845
|
+
} catch (error) {
|
|
846
|
+
if (isProviderError(error)) throw error;
|
|
847
|
+
if (error instanceof ProviderChoiceTokenError) throw error;
|
|
848
|
+
throw wordChoiceNotFoundError();
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
async function parseLegacyServerStoredChoice(options: {
|
|
400
853
|
readonly handle: ServerChoiceHandlePayload;
|
|
401
854
|
readonly storage?: ProviderChoiceStorageOptions;
|
|
402
855
|
readonly contextState?: ProviderRuntimeState;
|
|
@@ -406,9 +859,29 @@ async function parseServerStoredChoice(options: {
|
|
|
406
859
|
storage,
|
|
407
860
|
contextState: options.contextState,
|
|
408
861
|
});
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
)
|
|
862
|
+
// Reading a server-stored choice back deserializes a persisted value. A
|
|
863
|
+
// corrupt/undecodable value would otherwise surface as a raw JSON.parse
|
|
864
|
+
// SyntaxError (or another unexpected throwable) that escapes the choice error
|
|
865
|
+
// taxonomy, gets masked as internal_error 500, and is treated as retryable by
|
|
866
|
+
// the hub -> reservation restart loop (2026-07-22 catchtable RCA, candidate A).
|
|
867
|
+
// Convert any non-branded throwable into a branded invalid_payload so it maps
|
|
868
|
+
// to a clean, non-retryable 400. Branded ProviderChoiceTokenError and genuine
|
|
869
|
+
// ProviderError (e.g. Redis-unavailable / state-unavailable) pass through so
|
|
870
|
+
// their category/retryable semantics are preserved.
|
|
871
|
+
let record: StateValue<ProviderChoiceTokenPayload> | null;
|
|
872
|
+
try {
|
|
873
|
+
record = await namespace.get<ProviderChoiceTokenPayload>(
|
|
874
|
+
optionsStateKey(options.handle.state_id),
|
|
875
|
+
);
|
|
876
|
+
} catch (error) {
|
|
877
|
+
if (error instanceof ProviderChoiceTokenError || isProviderError(error)) {
|
|
878
|
+
throw error;
|
|
879
|
+
}
|
|
880
|
+
throw new ProviderChoiceTokenError(
|
|
881
|
+
"invalid_payload",
|
|
882
|
+
"Provider choice token state payload could not be decoded.",
|
|
883
|
+
);
|
|
884
|
+
}
|
|
412
885
|
if (!record) {
|
|
413
886
|
throw new ProviderChoiceTokenError(
|
|
414
887
|
"invalid_payload",
|
|
@@ -423,6 +896,83 @@ async function parseServerStoredChoice(options: {
|
|
|
423
896
|
return record.value;
|
|
424
897
|
}
|
|
425
898
|
|
|
899
|
+
function generateChoiceWordSequence(wordCount: number): string {
|
|
900
|
+
return Array.from({ length: wordCount }, () =>
|
|
901
|
+
choiceWordAt(randomInt(CHOICE_WORDLIST_SIZE)),
|
|
902
|
+
).join("-");
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
function parseWordChoiceStateKey(options: {
|
|
906
|
+
readonly token: string;
|
|
907
|
+
readonly prefix: string;
|
|
908
|
+
}): string | null {
|
|
909
|
+
if (!options.token.startsWith(options.prefix)) return null;
|
|
910
|
+
const body = options.token.slice(options.prefix.length);
|
|
911
|
+
// The official list contains one hyphenated entry (`yo-yo`), so structural
|
|
912
|
+
// recognition uses dictionary-aware segmentation instead of assuming every
|
|
913
|
+
// hyphen is a word boundary.
|
|
914
|
+
if (!/^[a-z]+(?:-[a-z]+){3,9}$/.test(body)) return null;
|
|
915
|
+
const segments = body.split("-");
|
|
916
|
+
if (
|
|
917
|
+
!canSegmentChoiceWords(segments, 0, STANDARD_CHOICE_WORD_COUNT) &&
|
|
918
|
+
!canSegmentChoiceWords(segments, 0, HIGH_CHOICE_WORD_COUNT)
|
|
919
|
+
) {
|
|
920
|
+
return null;
|
|
921
|
+
}
|
|
922
|
+
return body;
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
function canSegmentChoiceWords(
|
|
926
|
+
segments: readonly string[],
|
|
927
|
+
segmentIndex: number,
|
|
928
|
+
wordsRemaining: number,
|
|
929
|
+
): boolean {
|
|
930
|
+
if (wordsRemaining === 0) return segmentIndex === segments.length;
|
|
931
|
+
const segmentsRemaining = segments.length - segmentIndex;
|
|
932
|
+
if (segmentsRemaining < wordsRemaining) return false;
|
|
933
|
+
for (let end = segmentIndex + 1; end <= segments.length - (wordsRemaining - 1); end += 1) {
|
|
934
|
+
const candidate = segments.slice(segmentIndex, end).join("-");
|
|
935
|
+
if (candidate.length > 10) break;
|
|
936
|
+
if (isChoiceWord(candidate) && canSegmentChoiceWords(segments, end, wordsRemaining - 1)) {
|
|
937
|
+
return true;
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
return false;
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
function isServerStoredChoiceRecord(value: unknown): value is ServerStoredChoiceRecord {
|
|
944
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
945
|
+
return (
|
|
946
|
+
"v" in value &&
|
|
947
|
+
value.v === SERVER_STORED_CHOICE_RECORD_VERSION &&
|
|
948
|
+
"storage" in value &&
|
|
949
|
+
value.storage === "server" &&
|
|
950
|
+
"status" in value &&
|
|
951
|
+
(value.status === "active" || value.status === "consumed") &&
|
|
952
|
+
"provider_id" in value &&
|
|
953
|
+
typeof value.provider_id === "string" &&
|
|
954
|
+
"purpose" in value &&
|
|
955
|
+
typeof value.purpose === "string" &&
|
|
956
|
+
"issued_at_ms" in value &&
|
|
957
|
+
typeof value.issued_at_ms === "number" &&
|
|
958
|
+
"ttl_ms" in value &&
|
|
959
|
+
typeof value.ttl_ms === "number" &&
|
|
960
|
+
(!("binding" in value) || value.binding === undefined || isChoiceBinding(value.binding)) &&
|
|
961
|
+
"prefix" in value &&
|
|
962
|
+
typeof value.prefix === "string" &&
|
|
963
|
+
"payload" in value &&
|
|
964
|
+
isChoicePayload(value.payload) &&
|
|
965
|
+
"payload_digest" in value &&
|
|
966
|
+
typeof value.payload_digest === "string" &&
|
|
967
|
+
"replay_key" in value &&
|
|
968
|
+
typeof value.replay_key === "string"
|
|
969
|
+
);
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
function wordChoiceNotFoundError(): ProviderChoiceTokenError {
|
|
973
|
+
return new ProviderChoiceTokenError("invalid_payload", WORD_CHOICE_NOT_FOUND_MESSAGE);
|
|
974
|
+
}
|
|
975
|
+
|
|
426
976
|
type ServerProviderChoiceStorageOptions = Extract<
|
|
427
977
|
ProviderChoiceStorageOptions,
|
|
428
978
|
{ readonly mode: "server" | "auto" }
|
|
@@ -439,10 +989,7 @@ function resolveIssueStorage<TPayload extends ProviderChoiceTokenPayload>(
|
|
|
439
989
|
} {
|
|
440
990
|
if (!storage || storage.mode === "inline") return { mode: "inline" };
|
|
441
991
|
if (storage.mode === "server") return { mode: "server", storage };
|
|
442
|
-
const payloadBytes = Buffer.byteLength(
|
|
443
|
-
serializeChoicePayload(payload),
|
|
444
|
-
"utf8",
|
|
445
|
-
);
|
|
992
|
+
const payloadBytes = Buffer.byteLength(serializeChoicePayload(payload), "utf8");
|
|
446
993
|
if (payloadBytes <= storage.maxInlineBytes) return { mode: "inline" };
|
|
447
994
|
return { mode: "server", storage };
|
|
448
995
|
}
|
|
@@ -487,6 +1034,11 @@ function stateTtl(
|
|
|
487
1034
|
return storage.ttl ?? `${ttlMs ?? 1}ms`;
|
|
488
1035
|
}
|
|
489
1036
|
|
|
1037
|
+
function remainingStateTtl(expiresAt: string): ProviderStateDurationString {
|
|
1038
|
+
const remainingMs = Date.parse(expiresAt) - Date.now();
|
|
1039
|
+
return `${Number.isFinite(remainingMs) ? Math.max(1, Math.floor(remainingMs)) : 1}ms`;
|
|
1040
|
+
}
|
|
1041
|
+
|
|
490
1042
|
function optionsStateKey(stateId: string): string {
|
|
491
1043
|
return stateId;
|
|
492
1044
|
}
|
|
@@ -499,6 +1051,10 @@ function digestChoicePayload(serializedPayload: string): string {
|
|
|
499
1051
|
return createHash("sha256").update(serializedPayload).digest("base64url");
|
|
500
1052
|
}
|
|
501
1053
|
|
|
1054
|
+
function digestChoiceReplayKey(token: string): string {
|
|
1055
|
+
return createHash("sha256").update(token).digest("hex");
|
|
1056
|
+
}
|
|
1057
|
+
|
|
502
1058
|
function isServerChoiceHandlePayload(
|
|
503
1059
|
value: ProviderChoiceTokenPayload,
|
|
504
1060
|
): value is ServerChoiceHandlePayload {
|
|
@@ -536,10 +1092,7 @@ function parseManagedChoiceTokenParts(
|
|
|
536
1092
|
] {
|
|
537
1093
|
const parts = token.split(".");
|
|
538
1094
|
if (parts.length !== 6) {
|
|
539
|
-
throw new ProviderChoiceTokenError(
|
|
540
|
-
"invalid_shape",
|
|
541
|
-
"Provider choice token shape is invalid.",
|
|
542
|
-
);
|
|
1095
|
+
throw new ProviderChoiceTokenError("invalid_shape", "Provider choice token shape is invalid.");
|
|
543
1096
|
}
|
|
544
1097
|
return [parts[0], parts[1], parts[2], parts[3], parts[4], parts[5]];
|
|
545
1098
|
}
|
|
@@ -601,9 +1154,7 @@ function decryptManagedChoiceToken(options: {
|
|
|
601
1154
|
}
|
|
602
1155
|
}
|
|
603
1156
|
|
|
604
|
-
function isManagedChoiceEnvelope(
|
|
605
|
-
value: unknown,
|
|
606
|
-
): value is ManagedChoiceEnvelope {
|
|
1157
|
+
function isManagedChoiceEnvelope(value: unknown): value is ManagedChoiceEnvelope {
|
|
607
1158
|
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
608
1159
|
if (!("payload" in value) || !isChoicePayload(value.payload)) return false;
|
|
609
1160
|
return (
|
|
@@ -617,9 +1168,7 @@ function isManagedChoiceEnvelope(
|
|
|
617
1168
|
typeof value.issued_at_ms === "number" &&
|
|
618
1169
|
"ttl_ms" in value &&
|
|
619
1170
|
typeof value.ttl_ms === "number" &&
|
|
620
|
-
(!("binding" in value) ||
|
|
621
|
-
value.binding === undefined ||
|
|
622
|
-
isChoiceBinding(value.binding))
|
|
1171
|
+
(!("binding" in value) || value.binding === undefined || isChoiceBinding(value.binding))
|
|
623
1172
|
);
|
|
624
1173
|
}
|
|
625
1174
|
|
|
@@ -627,13 +1176,10 @@ function isChoicePayload(value: unknown): value is ProviderChoiceTokenPayload {
|
|
|
627
1176
|
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
628
1177
|
}
|
|
629
1178
|
|
|
630
|
-
function isChoiceBinding(
|
|
631
|
-
value: unknown,
|
|
632
|
-
): value is ManagedChoiceEnvelope["binding"] {
|
|
1179
|
+
function isChoiceBinding(value: unknown): value is ManagedChoiceEnvelope["binding"] {
|
|
633
1180
|
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
634
1181
|
return (
|
|
635
|
-
(!("connection_hash" in value) ||
|
|
636
|
-
typeof value.connection_hash === "string") &&
|
|
1182
|
+
(!("connection_hash" in value) || typeof value.connection_hash === "string") &&
|
|
637
1183
|
(!("credential_hash" in value) || typeof value.credential_hash === "string")
|
|
638
1184
|
);
|
|
639
1185
|
}
|
|
@@ -648,10 +1194,7 @@ function assertManagedChoiceEnvelope(
|
|
|
648
1194
|
readonly futureToleranceMs?: number;
|
|
649
1195
|
},
|
|
650
1196
|
): void {
|
|
651
|
-
if (
|
|
652
|
-
envelope.provider_id !== options.providerId ||
|
|
653
|
-
envelope.purpose !== options.purpose
|
|
654
|
-
) {
|
|
1197
|
+
if (envelope.provider_id !== options.providerId || envelope.purpose !== options.purpose) {
|
|
655
1198
|
throw new ProviderChoiceTokenError(
|
|
656
1199
|
"invalid_payload",
|
|
657
1200
|
"Provider choice token payload is invalid.",
|
|
@@ -660,10 +1203,7 @@ function assertManagedChoiceEnvelope(
|
|
|
660
1203
|
assertFreshProviderChoiceIssuedAt(envelope.issued_at_ms, {
|
|
661
1204
|
// Clamp to the issuer's embedded TTL so a caller-supplied value cannot
|
|
662
1205
|
// 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,
|
|
1206
|
+
ttlMs: options.ttlMs != null ? Math.min(options.ttlMs, envelope.ttl_ms) : envelope.ttl_ms,
|
|
667
1207
|
nowMs: options.nowMs,
|
|
668
1208
|
futureToleranceMs: options.futureToleranceMs,
|
|
669
1209
|
});
|
|
@@ -676,9 +1216,7 @@ function createChoiceBinding(options: {
|
|
|
676
1216
|
readonly credential?: CredentialContext;
|
|
677
1217
|
readonly required: boolean;
|
|
678
1218
|
}): ManagedChoiceEnvelope["binding"] {
|
|
679
|
-
const connectionHash = options.options?.connection
|
|
680
|
-
? hashRequiredConnection(options)
|
|
681
|
-
: undefined;
|
|
1219
|
+
const connectionHash = options.options?.connection ? hashRequiredConnection(options) : undefined;
|
|
682
1220
|
const credentialHash = options.options?.credentialKeys?.length
|
|
683
1221
|
? hashCredentialKeys(options)
|
|
684
1222
|
: undefined;
|
|
@@ -689,6 +1227,35 @@ function createChoiceBinding(options: {
|
|
|
689
1227
|
};
|
|
690
1228
|
}
|
|
691
1229
|
|
|
1230
|
+
function hasRequestedChoiceBinding(options?: ProviderChoiceBindingOptions): boolean {
|
|
1231
|
+
return options?.connection === true || Boolean(options?.credentialKeys?.length);
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
function assertWordChoiceBindingMatches(options: {
|
|
1235
|
+
readonly actual: ManagedChoiceEnvelope["binding"];
|
|
1236
|
+
readonly requested?: ProviderChoiceBindingOptions;
|
|
1237
|
+
readonly request?: ProviderRequestContext;
|
|
1238
|
+
readonly credential?: CredentialContext;
|
|
1239
|
+
readonly resolveKeys: () => ManagedChoiceKeys;
|
|
1240
|
+
}): void {
|
|
1241
|
+
const hasStoredBinding = Boolean(
|
|
1242
|
+
options.actual?.connection_hash || options.actual?.credential_hash,
|
|
1243
|
+
);
|
|
1244
|
+
const hasRequestedBinding = hasRequestedChoiceBinding(options.requested);
|
|
1245
|
+
if (!hasStoredBinding && !hasRequestedBinding) return;
|
|
1246
|
+
if (hasStoredBinding !== hasRequestedBinding) throw wordChoiceNotFoundError();
|
|
1247
|
+
assertChoiceBindingMatches({
|
|
1248
|
+
actual: options.actual,
|
|
1249
|
+
expected: createChoiceBinding({
|
|
1250
|
+
keys: options.resolveKeys(),
|
|
1251
|
+
options: options.requested,
|
|
1252
|
+
request: options.request,
|
|
1253
|
+
credential: options.credential,
|
|
1254
|
+
required: true,
|
|
1255
|
+
}),
|
|
1256
|
+
});
|
|
1257
|
+
}
|
|
1258
|
+
|
|
692
1259
|
function hashRequiredConnection(options: {
|
|
693
1260
|
readonly keys: ManagedChoiceKeys;
|
|
694
1261
|
readonly request?: ProviderRequestContext;
|
|
@@ -697,14 +1264,11 @@ function hashRequiredConnection(options: {
|
|
|
697
1264
|
const connectionId = options.request?.connectionId;
|
|
698
1265
|
if (!connectionId) {
|
|
699
1266
|
if (!options.required) return undefined;
|
|
700
|
-
throw new ProviderError(
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
retryable: false,
|
|
706
|
-
},
|
|
707
|
-
);
|
|
1267
|
+
throw new ProviderError("Provider choice tokens require connection context.", {
|
|
1268
|
+
code: "CHOICE_CONTEXT_REQUIRED",
|
|
1269
|
+
category: "input_validation",
|
|
1270
|
+
retryable: false,
|
|
1271
|
+
});
|
|
708
1272
|
}
|
|
709
1273
|
return createHmac("sha256", options.keys.binding)
|
|
710
1274
|
.update("connection")
|
|
@@ -722,15 +1286,12 @@ function hashCredentialKeys(options: {
|
|
|
722
1286
|
const material = credentialKeys.map((key) => {
|
|
723
1287
|
const value = options.credential?.get(key);
|
|
724
1288
|
if (typeof value !== "string" || value.length === 0) {
|
|
725
|
-
throw new ProviderError(
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
details: { credentialKey: key },
|
|
732
|
-
},
|
|
733
|
-
);
|
|
1289
|
+
throw new ProviderError("Provider choice tokens require configured credential binding.", {
|
|
1290
|
+
code: "CHOICE_CONTEXT_REQUIRED",
|
|
1291
|
+
category: "input_validation",
|
|
1292
|
+
retryable: false,
|
|
1293
|
+
details: { credentialKey: key },
|
|
1294
|
+
});
|
|
734
1295
|
}
|
|
735
1296
|
return [key, value];
|
|
736
1297
|
});
|