@apifuse/provider-sdk 2.2.0-beta.4 → 2.2.0-beta.40
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 +501 -13
- package/CHANGELOG.md +165 -1
- package/README.md +72 -14
- package/SUBMISSION.md +1 -1
- package/bin/apifuse-check.ts +88 -4
- package/bin/apifuse-dev.ts +38 -5
- package/bin/apifuse-pack-check.ts +22 -2
- package/bin/apifuse-pack-smoke.ts +57 -2
- package/bin/apifuse-pack-types.ts +356 -38
- package/bin/apifuse-perf.ts +14 -13
- package/bin/apifuse-record.ts +691 -68
- package/bin/apifuse-submit-check.ts +518 -37
- package/bin/apifuse-sync-assets.ts +117 -0
- package/bin/submit-check-delimited-text.ts +50 -0
- package/dist/auth-turn/index.d.ts +3 -3
- package/dist/auth-turn/index.js +1 -1
- package/dist/auth.d.ts +14 -0
- package/dist/auth.js +67 -0
- package/dist/ceremonies/index.d.ts +16 -0
- package/dist/ceremonies/index.js +141 -36
- 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/Dockerfile.tpl +1 -1
- package/dist/cli/templates/provider/README.md.tpl +4 -4
- package/dist/cli/templates/provider/index.ts.tpl +6 -3
- package/dist/cli/templates/provider/operations/ping.ts.tpl +2 -1
- package/dist/config/loader.d.ts +177 -16
- package/dist/config/loader.js +424 -127
- package/dist/contract-serialization.js +4 -8
- package/dist/contract-types.d.ts +1 -0
- package/dist/contract.js +2 -0
- package/dist/declaration-validation.d.ts +32 -0
- package/dist/declaration-validation.js +207 -0
- package/dist/define.d.ts +51 -25
- package/dist/define.js +752 -38
- package/dist/error-resolution.d.ts +4 -0
- package/dist/error-resolution.js +122 -0
- package/dist/errors.d.ts +18 -0
- package/dist/errors.js +40 -0
- package/dist/fixture-sanitization.d.ts +28 -0
- package/dist/fixture-sanitization.js +217 -0
- package/dist/health-scenario.d.ts +1842 -0
- package/dist/health-scenario.js +624 -0
- package/dist/index.d.ts +19 -9
- package/dist/index.js +10 -6
- package/dist/lint.d.ts +6 -1
- package/dist/lint.js +362 -3
- 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 +8 -2
- package/dist/provider.js +3 -1
- package/dist/runtime/auth-flow.d.ts +5 -1
- package/dist/runtime/auth-flow.js +6 -0
- package/dist/runtime/browser.d.ts +1 -0
- package/dist/runtime/browser.js +492 -49
- package/dist/runtime/cache.d.ts +1 -0
- package/dist/runtime/cache.js +169 -15
- 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 +490 -102
- package/dist/runtime/executor.d.ts +2 -2
- package/dist/runtime/executor.js +26 -2
- package/dist/runtime/http.d.ts +1 -0
- package/dist/runtime/http.js +515 -53
- package/dist/runtime/instrumentation.d.ts +2 -2
- package/dist/runtime/instrumentation.js +366 -8
- 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/proxy-errors.js +6 -2
- package/dist/runtime/proxy-nodemaven.d.ts +56 -0
- package/dist/runtime/proxy-nodemaven.js +146 -0
- package/dist/runtime/proxy-telemetry.d.ts +80 -1
- package/dist/runtime/proxy-telemetry.js +154 -47
- 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 +4 -2
- 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 +22 -0
- package/dist/runtime/resolver-vendors/browser.js +377 -0
- package/dist/runtime/resolver-vendors/capsolver.d.ts +22 -0
- package/dist/runtime/resolver-vendors/capsolver.js +526 -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 +407 -0
- package/dist/runtime/resolver-vendors/types.d.ts +94 -0
- package/dist/runtime/resolver-vendors/types.js +96 -0
- package/dist/runtime/resolver.d.ts +60 -0
- package/dist/runtime/resolver.js +737 -0
- package/dist/runtime/secrets.d.ts +27 -0
- package/dist/runtime/secrets.js +51 -0
- package/dist/runtime/state.d.ts +3 -0
- package/dist/runtime/state.js +277 -71
- package/dist/runtime/stealth-cookies.d.ts +20 -0
- package/dist/runtime/stealth-cookies.js +111 -0
- package/dist/runtime/stealth.d.ts +28 -3
- package/dist/runtime/stealth.js +519 -255
- package/dist/runtime/stt.js +1 -12
- package/dist/runtime/timeout.d.ts +5 -0
- package/dist/runtime/timeout.js +12 -0
- package/dist/runtime/trace-config.d.ts +12 -0
- package/dist/runtime/trace-config.js +61 -0
- package/dist/serve.d.ts +1 -1
- package/dist/serve.js +1 -1
- package/dist/server/index.d.ts +5 -3
- package/dist/server/index.js +3 -3
- 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.d.ts +114 -0
- package/dist/server/self-test.js +784 -148
- package/dist/server/serve-implementation.d.ts +213 -0
- package/dist/server/serve-implementation.js +2173 -0
- package/dist/server/serve.d.ts +1 -70
- package/dist/server/serve.js +1 -1130
- package/dist/server/trace-output.d.ts +4 -0
- package/dist/server/trace-output.js +20 -0
- package/dist/server/types.d.ts +30 -5
- package/dist/server/types.js +13 -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.js +16 -7
- package/dist/stream-evidence.d.ts +74 -0
- package/dist/stream-evidence.js +785 -0
- package/dist/stream.js +7 -1
- package/dist/testing/index.d.ts +2 -1
- package/dist/testing/index.js +2 -1
- package/dist/testing/run.d.ts +32 -2
- package/dist/testing/run.js +489 -21
- package/dist/trace-sanitization.d.ts +5 -0
- package/dist/trace-sanitization.js +45 -0
- package/dist/types.d.ts +545 -23
- package/dist/types.js +1 -0
- package/package.json +44 -5
- package/src/auth-turn/index.ts +1 -1
- package/src/auth.ts +118 -0
- package/src/ceremonies/index.ts +189 -46
- 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/Dockerfile.tpl +1 -1
- package/src/cli/templates/provider/README.md.tpl +4 -4
- package/src/cli/templates/provider/index.ts.tpl +6 -3
- package/src/cli/templates/provider/operations/ping.ts.tpl +2 -1
- package/src/config/loader.ts +665 -163
- package/src/contract-serialization.ts +5 -7
- package/src/contract-types.ts +1 -0
- package/src/contract.ts +2 -0
- package/src/declaration-validation.ts +266 -0
- package/src/define.ts +970 -87
- package/src/error-resolution.ts +127 -0
- package/src/errors.ts +52 -0
- package/src/fixture-sanitization.ts +248 -0
- package/src/health-scenario.ts +875 -0
- package/src/index.ts +204 -8
- package/src/lint.ts +408 -4
- 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 +133 -0
- package/src/runtime/auth-flow.ts +12 -0
- package/src/runtime/browser.ts +661 -63
- package/src/runtime/cache.ts +189 -14
- package/src/runtime/choice-wordlist.ts +145 -0
- package/src/runtime/choice.ts +631 -120
- package/src/runtime/executor.ts +40 -7
- package/src/runtime/http.ts +641 -61
- package/src/runtime/instrumentation.ts +520 -15
- 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/proxy-errors.ts +12 -4
- package/src/runtime/proxy-nodemaven.ts +221 -0
- package/src/runtime/proxy-telemetry.ts +244 -75
- package/src/runtime/redirects.ts +66 -0
- package/src/runtime/redis.ts +7 -2
- package/src/runtime/request-options.ts +680 -1
- package/src/runtime/resolver-config.ts +6 -0
- package/src/runtime/resolver-public.ts +20 -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 +533 -0
- package/src/runtime/resolver-vendors/capsolver.ts +700 -0
- package/src/runtime/resolver-vendors/hosts.ts +38 -0
- package/src/runtime/resolver-vendors/twocaptcha.ts +539 -0
- package/src/runtime/resolver-vendors/types.ts +212 -0
- package/src/runtime/resolver.ts +1103 -0
- package/src/runtime/secrets.ts +64 -0
- package/src/runtime/state.ts +394 -77
- package/src/runtime/stealth-cookies.ts +132 -0
- package/src/runtime/stealth.ts +675 -289
- package/src/runtime/stt.ts +1 -19
- package/src/runtime/timeout.ts +18 -0
- package/src/runtime/trace-config.ts +77 -0
- package/src/serve.ts +6 -1
- package/src/server/index.ts +37 -2
- package/src/server/self-test-input-tokens.ts +29 -14
- package/src/server/self-test.ts +1025 -175
- package/src/server/serve-implementation.ts +3250 -0
- package/src/server/serve.ts +1 -1626
- package/src/server/trace-output.ts +32 -0
- package/src/server/types.ts +13 -1
- 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 +17 -7
- package/src/stream-evidence.ts +988 -0
- package/src/stream.ts +8 -1
- package/src/testing/index.ts +10 -1
- package/src/testing/run.ts +658 -15
- package/src/trace-sanitization.ts +63 -0
- package/src/types.ts +660 -38
- package/dist/cli/templates/provider/CLAUDE.md.tpl +0 -1
- package/src/cli/templates/provider/CLAUDE.md.tpl +0 -1
- /package/dist/cli/templates/provider/{skills → .agents/skills}/fixtures-and-recording/SKILL.md.tpl +0 -0
- /package/dist/cli/templates/provider/{skills → .agents/skills}/health-checks-and-fail-closed/SKILL.md.tpl +0 -0
- /package/dist/cli/templates/provider/{skills → .agents/skills}/normalization-standards/SKILL.md.tpl +0 -0
- /package/dist/cli/templates/provider/{skills → .agents/skills}/pagination-and-counts/SKILL.md.tpl +0 -0
- /package/dist/cli/templates/provider/{skills → .agents/skills}/upstream-contract-verification/SKILL.md.tpl +0 -0
- /package/dist/cli/templates/provider/{skills → .agents/skills}/upstream-notes/README.md.tpl +0 -0
- /package/src/cli/templates/provider/{skills → .agents/skills}/fixtures-and-recording/SKILL.md.tpl +0 -0
- /package/src/cli/templates/provider/{skills → .agents/skills}/health-checks-and-fail-closed/SKILL.md.tpl +0 -0
- /package/src/cli/templates/provider/{skills → .agents/skills}/normalization-standards/SKILL.md.tpl +0 -0
- /package/src/cli/templates/provider/{skills → .agents/skills}/pagination-and-counts/SKILL.md.tpl +0 -0
- /package/src/cli/templates/provider/{skills → .agents/skills}/upstream-contract-verification/SKILL.md.tpl +0 -0
- /package/src/cli/templates/provider/{skills → .agents/skills}/upstream-notes/README.md.tpl +0 -0
package/dist/runtime/choice.js
CHANGED
|
@@ -1,14 +1,52 @@
|
|
|
1
|
-
import { createCipheriv, createDecipheriv, createHash, createHmac, randomBytes, timingSafeEqual, } from "node:crypto";
|
|
1
|
+
import { createCipheriv, createDecipheriv, createHash, createHmac, randomBytes, randomInt, timingSafeEqual, } from "node:crypto";
|
|
2
2
|
import { assertFreshProviderChoiceIssuedAt, ProviderChoiceTokenError, } from "../choice-token.js";
|
|
3
|
-
import { ProviderError } from "../errors.js";
|
|
3
|
+
import { isProviderError, ProviderError } from "../errors.js";
|
|
4
|
+
import { CHOICE_WORDLIST_SIZE, choiceWordAt, HIGH_CHOICE_WORD_COUNT, isChoiceWord, STANDARD_CHOICE_WORD_COUNT, } from "./choice-wordlist.js";
|
|
4
5
|
export const PROVIDER_RUNTIME_CHOICE_TOKEN_MASTER_SECRET_ENV = "APIFUSE__PROVIDER_RUNTIME__CHOICE_TOKEN_MASTER_SECRET";
|
|
5
6
|
const PRIMARY_CHOICE_TOKEN_KID = "v1";
|
|
6
7
|
const MANAGED_CHOICE_TOKEN_VERSION = 1;
|
|
8
|
+
const SERVER_STORED_CHOICE_RECORD_VERSION = 1;
|
|
9
|
+
const SERVER_STORED_CHOICE_ISSUE_ATTEMPTS = 5;
|
|
10
|
+
const WORD_CHOICE_NOT_FOUND_MESSAGE = "Provider choice token was not found.";
|
|
7
11
|
export function createProviderChoiceContext(options) {
|
|
8
12
|
const kid = options.kid ?? PRIMARY_CHOICE_TOKEN_KID;
|
|
9
13
|
const resolveMasterSecret = () => resolveChoiceMasterSecret(options);
|
|
10
14
|
function issue(issueOptions) {
|
|
11
15
|
const issuedAtMs = issueOptions.nowMs ?? Date.now();
|
|
16
|
+
const resolvedStorage = resolveIssueStorage(issueOptions.storage, issueOptions.payload);
|
|
17
|
+
if (resolvedStorage.mode === "server") {
|
|
18
|
+
const keys = hasRequestedChoiceBinding(issueOptions.bind)
|
|
19
|
+
? deriveManagedChoiceKeys({
|
|
20
|
+
masterSecret: resolveMasterSecret(),
|
|
21
|
+
providerId: options.providerId,
|
|
22
|
+
purpose: issueOptions.purpose,
|
|
23
|
+
kid,
|
|
24
|
+
})
|
|
25
|
+
: undefined;
|
|
26
|
+
const binding = hasRequestedChoiceBinding(issueOptions.bind)
|
|
27
|
+
? createChoiceBinding({
|
|
28
|
+
keys: keys,
|
|
29
|
+
options: issueOptions.bind,
|
|
30
|
+
request: options.request,
|
|
31
|
+
credential: options.credential,
|
|
32
|
+
required: true,
|
|
33
|
+
})
|
|
34
|
+
: undefined;
|
|
35
|
+
const baseEnvelope = {
|
|
36
|
+
v: MANAGED_CHOICE_TOKEN_VERSION,
|
|
37
|
+
provider_id: options.providerId,
|
|
38
|
+
purpose: issueOptions.purpose,
|
|
39
|
+
issued_at_ms: issuedAtMs,
|
|
40
|
+
ttl_ms: issueOptions.ttlMs,
|
|
41
|
+
binding,
|
|
42
|
+
};
|
|
43
|
+
return issueWordServerStoredChoice({
|
|
44
|
+
baseEnvelope,
|
|
45
|
+
issueOptions,
|
|
46
|
+
storage: resolvedStorage.storage,
|
|
47
|
+
contextState: options.state,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
12
50
|
const keys = deriveManagedChoiceKeys({
|
|
13
51
|
masterSecret: resolveMasterSecret(),
|
|
14
52
|
providerId: options.providerId,
|
|
@@ -29,18 +67,6 @@ export function createProviderChoiceContext(options) {
|
|
|
29
67
|
required: true,
|
|
30
68
|
}),
|
|
31
69
|
};
|
|
32
|
-
const resolvedStorage = resolveIssueStorage(issueOptions.storage, issueOptions.payload);
|
|
33
|
-
if (resolvedStorage.mode === "server") {
|
|
34
|
-
return issueServerStoredChoice({
|
|
35
|
-
baseEnvelope,
|
|
36
|
-
issueOptions,
|
|
37
|
-
storage: resolvedStorage.storage,
|
|
38
|
-
contextState: options.state,
|
|
39
|
-
kid,
|
|
40
|
-
keys,
|
|
41
|
-
issuedAtMs,
|
|
42
|
-
});
|
|
43
|
-
}
|
|
44
70
|
const envelope = {
|
|
45
71
|
...baseEnvelope,
|
|
46
72
|
payload: issueOptions.payload,
|
|
@@ -53,58 +79,129 @@ export function createProviderChoiceContext(options) {
|
|
|
53
79
|
});
|
|
54
80
|
}
|
|
55
81
|
function parse(parseOptions) {
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
!encryptedPayload ||
|
|
61
|
-
!authTag ||
|
|
62
|
-
!signature) {
|
|
63
|
-
throw new ProviderChoiceTokenError("invalid_shape", "Provider choice token shape is invalid.");
|
|
64
|
-
}
|
|
65
|
-
const keys = deriveManagedChoiceKeys({
|
|
66
|
-
masterSecret: resolveMasterSecret(),
|
|
67
|
-
providerId: options.providerId,
|
|
68
|
-
purpose: parseOptions.purpose,
|
|
69
|
-
kid: tokenKid,
|
|
70
|
-
});
|
|
71
|
-
const signedBody = [parseOptions.prefix, tokenKid, encodedIv, encryptedPayload, authTag].join(".");
|
|
72
|
-
assertManagedChoiceSignature({
|
|
73
|
-
signedBody,
|
|
74
|
-
signature,
|
|
75
|
-
signingKey: keys.signing,
|
|
76
|
-
});
|
|
77
|
-
const envelope = decryptManagedChoiceToken({
|
|
78
|
-
encodedIv,
|
|
79
|
-
encryptedPayload,
|
|
80
|
-
authTag,
|
|
81
|
-
encryptionKey: keys.encryption,
|
|
82
|
+
const consumeMode = parseOptions.consume ?? "never";
|
|
83
|
+
const wordStateKey = parseWordChoiceStateKey({
|
|
84
|
+
token: parseOptions.token,
|
|
85
|
+
prefix: parseOptions.prefix,
|
|
82
86
|
});
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
});
|
|
90
|
-
assertChoiceBindingMatches({
|
|
91
|
-
actual: envelope.binding,
|
|
92
|
-
expected: createChoiceBinding({
|
|
93
|
-
keys,
|
|
94
|
-
options: parseOptions.bind,
|
|
87
|
+
if (wordStateKey) {
|
|
88
|
+
const parsed = parseWordServerStoredChoice({
|
|
89
|
+
stateKey: wordStateKey,
|
|
90
|
+
parseOptions,
|
|
91
|
+
contextState: options.state,
|
|
92
|
+
providerId: options.providerId,
|
|
95
93
|
request: options.request,
|
|
96
94
|
credential: options.credential,
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
95
|
+
resolveBindingKeys: () => deriveManagedChoiceKeys({
|
|
96
|
+
masterSecret: resolveMasterSecret(),
|
|
97
|
+
providerId: options.providerId,
|
|
98
|
+
purpose: parseOptions.purpose,
|
|
99
|
+
kid,
|
|
100
|
+
}),
|
|
101
|
+
onConsume: (result) => emitChoiceTelemetry(options.onTelemetry, {
|
|
102
|
+
providerId: options.providerId,
|
|
103
|
+
purpose: parseOptions.purpose,
|
|
104
|
+
operation: "consume",
|
|
105
|
+
format: "word",
|
|
106
|
+
outcome: "success",
|
|
107
|
+
consumeMode,
|
|
108
|
+
consumed: result.status === "consumed",
|
|
109
|
+
replay: result.status === "already-consumed",
|
|
110
|
+
}),
|
|
111
|
+
});
|
|
112
|
+
return observeChoiceParse(parsed, {
|
|
113
|
+
onTelemetry: options.onTelemetry,
|
|
114
|
+
providerId: options.providerId,
|
|
115
|
+
purpose: parseOptions.purpose,
|
|
116
|
+
format: "word",
|
|
117
|
+
consumeMode,
|
|
105
118
|
});
|
|
106
119
|
}
|
|
107
|
-
|
|
120
|
+
// Inline choices continue to use the encrypted envelope. A structurally
|
|
121
|
+
// valid word token returns above, so lookup, expiry, consumption, and
|
|
122
|
+
// binding failures can never enter this branch.
|
|
123
|
+
try {
|
|
124
|
+
const [actualPrefix, tokenKid, encodedIv, encryptedPayload, authTag, signature] = parseManagedChoiceTokenParts(parseOptions.token);
|
|
125
|
+
if (actualPrefix !== parseOptions.prefix ||
|
|
126
|
+
tokenKid !== kid ||
|
|
127
|
+
!encodedIv ||
|
|
128
|
+
!encryptedPayload ||
|
|
129
|
+
!authTag ||
|
|
130
|
+
!signature) {
|
|
131
|
+
throw new ProviderChoiceTokenError("invalid_shape", "Provider choice token shape is invalid.");
|
|
132
|
+
}
|
|
133
|
+
const keys = deriveManagedChoiceKeys({
|
|
134
|
+
masterSecret: resolveMasterSecret(),
|
|
135
|
+
providerId: options.providerId,
|
|
136
|
+
purpose: parseOptions.purpose,
|
|
137
|
+
kid: tokenKid,
|
|
138
|
+
});
|
|
139
|
+
const signedBody = [parseOptions.prefix, tokenKid, encodedIv, encryptedPayload, authTag].join(".");
|
|
140
|
+
assertManagedChoiceSignature({
|
|
141
|
+
signedBody,
|
|
142
|
+
signature,
|
|
143
|
+
signingKey: keys.signing,
|
|
144
|
+
});
|
|
145
|
+
const envelope = decryptManagedChoiceToken({
|
|
146
|
+
encodedIv,
|
|
147
|
+
encryptedPayload,
|
|
148
|
+
authTag,
|
|
149
|
+
encryptionKey: keys.encryption,
|
|
150
|
+
});
|
|
151
|
+
assertManagedChoiceEnvelope(envelope, {
|
|
152
|
+
providerId: options.providerId,
|
|
153
|
+
purpose: parseOptions.purpose,
|
|
154
|
+
ttlMs: parseOptions.ttlMs,
|
|
155
|
+
nowMs: parseOptions.nowMs,
|
|
156
|
+
futureToleranceMs: parseOptions.futureToleranceMs,
|
|
157
|
+
});
|
|
158
|
+
assertChoiceBindingMatches({
|
|
159
|
+
actual: envelope.binding,
|
|
160
|
+
expected: createChoiceBinding({
|
|
161
|
+
keys,
|
|
162
|
+
options: parseOptions.bind,
|
|
163
|
+
request: options.request,
|
|
164
|
+
credential: options.credential,
|
|
165
|
+
required: true,
|
|
166
|
+
}),
|
|
167
|
+
});
|
|
168
|
+
if (isServerChoiceHandlePayload(envelope.payload)) {
|
|
169
|
+
throw wordChoiceNotFoundError();
|
|
170
|
+
}
|
|
171
|
+
const payload = envelope.payload;
|
|
172
|
+
const parsed = consumeMode === "explicit"
|
|
173
|
+
? Promise.resolve(payload).then((resolvedPayload) => createInlineExplicitParseResult({
|
|
174
|
+
payload: resolvedPayload,
|
|
175
|
+
replayKey: digestChoiceReplayKey(parseOptions.token),
|
|
176
|
+
onConsume: () => emitChoiceTelemetry(options.onTelemetry, {
|
|
177
|
+
providerId: options.providerId,
|
|
178
|
+
purpose: parseOptions.purpose,
|
|
179
|
+
operation: "consume",
|
|
180
|
+
format: "legacy",
|
|
181
|
+
outcome: "unsupported",
|
|
182
|
+
consumeMode,
|
|
183
|
+
consumed: false,
|
|
184
|
+
replay: false,
|
|
185
|
+
}),
|
|
186
|
+
}))
|
|
187
|
+
: payload;
|
|
188
|
+
return observeChoiceParse(parsed, {
|
|
189
|
+
onTelemetry: options.onTelemetry,
|
|
190
|
+
providerId: options.providerId,
|
|
191
|
+
purpose: parseOptions.purpose,
|
|
192
|
+
format: "legacy",
|
|
193
|
+
consumeMode,
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
catch (error) {
|
|
197
|
+
emitChoiceParseFailure(options.onTelemetry, error, {
|
|
198
|
+
providerId: options.providerId,
|
|
199
|
+
purpose: parseOptions.purpose,
|
|
200
|
+
format: "legacy",
|
|
201
|
+
consumeMode,
|
|
202
|
+
});
|
|
203
|
+
throw error;
|
|
204
|
+
}
|
|
108
205
|
}
|
|
109
206
|
return { issue, parse };
|
|
110
207
|
}
|
|
@@ -114,6 +211,76 @@ export function createTestProviderChoiceContext(options) {
|
|
|
114
211
|
masterSecret: options.masterSecret ?? "apifuse-test-provider-runtime-choice-token-master-secret",
|
|
115
212
|
});
|
|
116
213
|
}
|
|
214
|
+
function observeChoiceParse(result, base) {
|
|
215
|
+
if (result instanceof Promise) {
|
|
216
|
+
return result.then((value) => {
|
|
217
|
+
emitChoiceParseSuccess(base, value);
|
|
218
|
+
return value;
|
|
219
|
+
}, (error) => {
|
|
220
|
+
emitChoiceParseFailure(base.onTelemetry, error, base);
|
|
221
|
+
throw error;
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
emitChoiceParseSuccess(base, result);
|
|
225
|
+
return result;
|
|
226
|
+
}
|
|
227
|
+
function emitChoiceParseSuccess(base, result) {
|
|
228
|
+
const replay = isConsumedChoiceReplay(result);
|
|
229
|
+
emitChoiceTelemetry(base.onTelemetry, {
|
|
230
|
+
providerId: base.providerId,
|
|
231
|
+
purpose: base.purpose,
|
|
232
|
+
operation: "parse",
|
|
233
|
+
format: base.format,
|
|
234
|
+
outcome: "success",
|
|
235
|
+
consumeMode: base.consumeMode,
|
|
236
|
+
consumed: replay || (base.format === "word" && base.consumeMode === "on-parse"),
|
|
237
|
+
replay,
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
function isConsumedChoiceReplay(value) {
|
|
241
|
+
return (value !== null &&
|
|
242
|
+
typeof value === "object" &&
|
|
243
|
+
"status" in value &&
|
|
244
|
+
value.status === "consumed" &&
|
|
245
|
+
"replayKey" in value &&
|
|
246
|
+
typeof value.replayKey === "string");
|
|
247
|
+
}
|
|
248
|
+
function emitChoiceParseFailure(onTelemetry, error, base) {
|
|
249
|
+
const outcome = error instanceof ProviderChoiceTokenError
|
|
250
|
+
? base.format === "word" && error.message === WORD_CHOICE_NOT_FOUND_MESSAGE
|
|
251
|
+
? "not-found"
|
|
252
|
+
: "invalid"
|
|
253
|
+
: "error";
|
|
254
|
+
emitChoiceTelemetry(onTelemetry, {
|
|
255
|
+
providerId: base.providerId,
|
|
256
|
+
purpose: base.purpose,
|
|
257
|
+
operation: "parse",
|
|
258
|
+
format: base.format,
|
|
259
|
+
outcome,
|
|
260
|
+
consumeMode: base.consumeMode,
|
|
261
|
+
consumed: false,
|
|
262
|
+
replay: false,
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
function emitChoiceTelemetry(onTelemetry, event) {
|
|
266
|
+
try {
|
|
267
|
+
onTelemetry?.(event);
|
|
268
|
+
}
|
|
269
|
+
catch {
|
|
270
|
+
// Observability must never change provider token semantics.
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
function createInlineExplicitParseResult(options) {
|
|
274
|
+
return {
|
|
275
|
+
status: "active",
|
|
276
|
+
payload: options.payload,
|
|
277
|
+
replayKey: options.replayKey,
|
|
278
|
+
consume: async () => {
|
|
279
|
+
options.onConsume();
|
|
280
|
+
return { status: "unsupported" };
|
|
281
|
+
},
|
|
282
|
+
};
|
|
283
|
+
}
|
|
117
284
|
function resolveChoiceMasterSecret(options) {
|
|
118
285
|
const configured = options.masterSecret ?? options.env?.get(PROVIDER_RUNTIME_CHOICE_TOKEN_MASTER_SECRET_ENV);
|
|
119
286
|
const trimmed = configured?.trim();
|
|
@@ -163,62 +330,255 @@ function encryptManagedChoiceToken(options) {
|
|
|
163
330
|
.digest("base64url");
|
|
164
331
|
return `${signedBody}.${signature}`;
|
|
165
332
|
}
|
|
166
|
-
async function
|
|
333
|
+
async function issueWordServerStoredChoice(options) {
|
|
167
334
|
const serializedPayload = serializeChoicePayload(options.issueOptions.payload);
|
|
168
|
-
const
|
|
169
|
-
if (payloadBytes > options.storage.maxValueBytes) {
|
|
170
|
-
throw new ProviderError("Provider choice payload exceeds state storage policy.", {
|
|
171
|
-
code: "CHOICE_STATE_PAYLOAD_TOO_LARGE",
|
|
172
|
-
category: "input_validation",
|
|
173
|
-
retryable: false,
|
|
174
|
-
details: {
|
|
175
|
-
maxValueBytes: options.storage.maxValueBytes,
|
|
176
|
-
payloadBytes,
|
|
177
|
-
},
|
|
178
|
-
});
|
|
179
|
-
}
|
|
180
|
-
const stateId = `choice_${randomBytes(16).toString("base64url")}`;
|
|
181
|
-
const digest = digestChoicePayload(serializedPayload);
|
|
335
|
+
const payloadDigest = digestChoicePayload(serializedPayload);
|
|
182
336
|
const namespace = resolveChoiceStateNamespace({
|
|
183
337
|
storage: options.storage,
|
|
184
338
|
contextState: options.contextState,
|
|
185
339
|
ttlMs: options.issueOptions.ttlMs,
|
|
186
340
|
});
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
341
|
+
const wordCount = options.issueOptions.strength === "high" ? HIGH_CHOICE_WORD_COUNT : STANDARD_CHOICE_WORD_COUNT;
|
|
342
|
+
for (let attempt = 0; attempt < SERVER_STORED_CHOICE_ISSUE_ATTEMPTS; attempt += 1) {
|
|
343
|
+
const stateKey = generateChoiceWordSequence(wordCount);
|
|
344
|
+
const token = `${options.issueOptions.prefix}${stateKey}`;
|
|
345
|
+
const record = {
|
|
346
|
+
v: SERVER_STORED_CHOICE_RECORD_VERSION,
|
|
193
347
|
storage: "server",
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
348
|
+
status: "active",
|
|
349
|
+
provider_id: options.baseEnvelope.provider_id,
|
|
350
|
+
purpose: options.baseEnvelope.purpose,
|
|
351
|
+
issued_at_ms: options.baseEnvelope.issued_at_ms,
|
|
352
|
+
ttl_ms: options.baseEnvelope.ttl_ms,
|
|
353
|
+
binding: options.baseEnvelope.binding,
|
|
354
|
+
prefix: options.issueOptions.prefix,
|
|
355
|
+
payload: options.issueOptions.payload,
|
|
356
|
+
payload_digest: payloadDigest,
|
|
357
|
+
replay_key: digestChoiceReplayKey(token),
|
|
358
|
+
};
|
|
359
|
+
const valueBytes = Buffer.byteLength(JSON.stringify(record), "utf8");
|
|
360
|
+
if (valueBytes > options.storage.maxValueBytes) {
|
|
361
|
+
throw new ProviderError("Provider choice payload exceeds state storage policy.", {
|
|
362
|
+
code: "CHOICE_STATE_PAYLOAD_TOO_LARGE",
|
|
363
|
+
category: "input_validation",
|
|
364
|
+
retryable: false,
|
|
365
|
+
details: {
|
|
366
|
+
maxValueBytes: options.storage.maxValueBytes,
|
|
367
|
+
valueBytes,
|
|
368
|
+
},
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
const result = await namespace.compareAndSet(optionsStateKey(stateKey), 0, record, {
|
|
372
|
+
ttl: stateTtl(options.storage, options.issueOptions.ttlMs),
|
|
373
|
+
});
|
|
374
|
+
if (result.ok)
|
|
375
|
+
return token;
|
|
376
|
+
}
|
|
377
|
+
throw new ProviderError("Provider choice state storage is not available.", {
|
|
378
|
+
code: "CHOICE_STATE_UNAVAILABLE",
|
|
379
|
+
category: "internal_error",
|
|
380
|
+
retryable: false,
|
|
204
381
|
});
|
|
205
382
|
}
|
|
206
|
-
async function
|
|
207
|
-
const storage = resolveParseStorage(options.storage);
|
|
383
|
+
async function parseWordServerStoredChoice(options) {
|
|
384
|
+
const storage = resolveParseStorage(options.parseOptions.storage);
|
|
208
385
|
const namespace = resolveChoiceStateNamespace({
|
|
209
386
|
storage,
|
|
210
387
|
contextState: options.contextState,
|
|
388
|
+
ttlMs: options.parseOptions.ttlMs,
|
|
211
389
|
});
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
390
|
+
let stored;
|
|
391
|
+
try {
|
|
392
|
+
stored = await namespace.get(optionsStateKey(options.stateKey));
|
|
393
|
+
}
|
|
394
|
+
catch (error) {
|
|
395
|
+
if (isProviderError(error))
|
|
396
|
+
throw error;
|
|
397
|
+
throw wordChoiceNotFoundError();
|
|
398
|
+
}
|
|
399
|
+
if (!stored || !isServerStoredChoiceRecord(stored.value)) {
|
|
400
|
+
throw wordChoiceNotFoundError();
|
|
401
|
+
}
|
|
402
|
+
const record = stored.value;
|
|
403
|
+
const expectedReplayKey = digestChoiceReplayKey(`${options.parseOptions.prefix}${options.stateKey}`);
|
|
404
|
+
try {
|
|
405
|
+
if (record.provider_id !== options.providerId ||
|
|
406
|
+
record.purpose !== options.parseOptions.purpose ||
|
|
407
|
+
record.prefix !== options.parseOptions.prefix) {
|
|
408
|
+
throw wordChoiceNotFoundError();
|
|
409
|
+
}
|
|
410
|
+
assertPayloadDigestMatches({
|
|
411
|
+
actual: digestChoicePayload(serializeChoicePayload(record.payload)),
|
|
412
|
+
expected: record.payload_digest,
|
|
413
|
+
});
|
|
414
|
+
assertPayloadDigestMatches({ actual: expectedReplayKey, expected: record.replay_key });
|
|
415
|
+
assertWordChoiceBindingMatches({
|
|
416
|
+
actual: record.binding,
|
|
417
|
+
requested: options.parseOptions.bind,
|
|
418
|
+
request: options.request,
|
|
419
|
+
credential: options.credential,
|
|
420
|
+
resolveKeys: options.resolveBindingKeys,
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
catch (error) {
|
|
424
|
+
if (error instanceof ProviderChoiceTokenError ||
|
|
425
|
+
(isProviderError(error) && error.code === "CHOICE_CONTEXT_REQUIRED")) {
|
|
426
|
+
throw wordChoiceNotFoundError();
|
|
427
|
+
}
|
|
428
|
+
throw error;
|
|
215
429
|
}
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
430
|
+
// Freshness is classified last, reachable only after every identity,
|
|
431
|
+
// integrity, and binding check above has passed (ADR 0006, amended
|
|
432
|
+
// 2026-08-20): a caller that proved the record's binding may observe the
|
|
433
|
+
// canonical stale error, while an unbound record keeps the collapsed
|
|
434
|
+
// not-found error so expiry never becomes an existence signal for
|
|
435
|
+
// guessable tokens.
|
|
436
|
+
try {
|
|
437
|
+
assertFreshProviderChoiceIssuedAt(record.issued_at_ms, {
|
|
438
|
+
ttlMs: options.parseOptions.ttlMs != null
|
|
439
|
+
? Math.min(options.parseOptions.ttlMs, record.ttl_ms)
|
|
440
|
+
: record.ttl_ms,
|
|
441
|
+
nowMs: options.parseOptions.nowMs,
|
|
442
|
+
futureToleranceMs: options.parseOptions.futureToleranceMs,
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
catch (error) {
|
|
446
|
+
const recordIsBound = Boolean(record.binding?.connection_hash || record.binding?.credential_hash);
|
|
447
|
+
if (recordIsBound && error instanceof ProviderChoiceTokenError && error.reason === "stale") {
|
|
448
|
+
throw error;
|
|
449
|
+
}
|
|
450
|
+
if (error instanceof ProviderChoiceTokenError)
|
|
451
|
+
throw wordChoiceNotFoundError();
|
|
452
|
+
throw error;
|
|
453
|
+
}
|
|
454
|
+
const consumeMode = options.parseOptions.consume ?? "never";
|
|
455
|
+
if (record.status === "consumed") {
|
|
456
|
+
if (consumeMode === "explicit") {
|
|
457
|
+
return { status: "consumed", replayKey: record.replay_key };
|
|
458
|
+
}
|
|
459
|
+
throw wordChoiceNotFoundError();
|
|
460
|
+
}
|
|
461
|
+
if (consumeMode === "never")
|
|
462
|
+
return record.payload;
|
|
463
|
+
if (consumeMode === "explicit") {
|
|
464
|
+
return {
|
|
465
|
+
status: "active",
|
|
466
|
+
payload: record.payload,
|
|
467
|
+
replayKey: record.replay_key,
|
|
468
|
+
consume: async () => {
|
|
469
|
+
const result = await consumeWordServerStoredChoice({
|
|
470
|
+
stateKey: options.stateKey,
|
|
471
|
+
stored,
|
|
472
|
+
record,
|
|
473
|
+
storage,
|
|
474
|
+
contextState: options.contextState,
|
|
475
|
+
});
|
|
476
|
+
options.onConsume(result);
|
|
477
|
+
return result;
|
|
478
|
+
},
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
const consumed = await consumeWordServerStoredChoice({
|
|
482
|
+
stateKey: options.stateKey,
|
|
483
|
+
stored,
|
|
484
|
+
record,
|
|
485
|
+
storage,
|
|
486
|
+
contextState: options.contextState,
|
|
487
|
+
});
|
|
488
|
+
if (consumed.status !== "consumed")
|
|
489
|
+
throw wordChoiceNotFoundError();
|
|
490
|
+
return record.payload;
|
|
491
|
+
}
|
|
492
|
+
async function consumeWordServerStoredChoice(options) {
|
|
493
|
+
const namespace = resolveChoiceStateNamespace({
|
|
494
|
+
storage: options.storage,
|
|
495
|
+
contextState: options.contextState,
|
|
496
|
+
ttlMs: options.record.ttl_ms,
|
|
220
497
|
});
|
|
221
|
-
|
|
498
|
+
try {
|
|
499
|
+
const consumed = await namespace.compareAndSet(optionsStateKey(options.stateKey), options.stored.version, { ...options.record, status: "consumed" }, { ttl: remainingStateTtl(options.stored.expiresAt) });
|
|
500
|
+
if (consumed.ok)
|
|
501
|
+
return { status: "consumed" };
|
|
502
|
+
if (consumed.current &&
|
|
503
|
+
isServerStoredChoiceRecord(consumed.current.value) &&
|
|
504
|
+
consumed.current.value.status === "consumed" &&
|
|
505
|
+
consumed.current.value.replay_key === options.record.replay_key) {
|
|
506
|
+
return { status: "already-consumed" };
|
|
507
|
+
}
|
|
508
|
+
throw wordChoiceNotFoundError();
|
|
509
|
+
}
|
|
510
|
+
catch (error) {
|
|
511
|
+
if (isProviderError(error))
|
|
512
|
+
throw error;
|
|
513
|
+
if (error instanceof ProviderChoiceTokenError)
|
|
514
|
+
throw error;
|
|
515
|
+
throw wordChoiceNotFoundError();
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
function generateChoiceWordSequence(wordCount) {
|
|
519
|
+
return Array.from({ length: wordCount }, () => choiceWordAt(randomInt(CHOICE_WORDLIST_SIZE))).join("-");
|
|
520
|
+
}
|
|
521
|
+
function parseWordChoiceStateKey(options) {
|
|
522
|
+
if (!options.token.startsWith(options.prefix))
|
|
523
|
+
return null;
|
|
524
|
+
const body = options.token.slice(options.prefix.length);
|
|
525
|
+
// The official list contains one hyphenated entry (`yo-yo`), so structural
|
|
526
|
+
// recognition uses dictionary-aware segmentation instead of assuming every
|
|
527
|
+
// hyphen is a word boundary.
|
|
528
|
+
if (!/^[a-z]+(?:-[a-z]+){3,9}$/.test(body))
|
|
529
|
+
return null;
|
|
530
|
+
const segments = body.split("-");
|
|
531
|
+
if (!canSegmentChoiceWords(segments, 0, STANDARD_CHOICE_WORD_COUNT) &&
|
|
532
|
+
!canSegmentChoiceWords(segments, 0, HIGH_CHOICE_WORD_COUNT)) {
|
|
533
|
+
return null;
|
|
534
|
+
}
|
|
535
|
+
return body;
|
|
536
|
+
}
|
|
537
|
+
function canSegmentChoiceWords(segments, segmentIndex, wordsRemaining) {
|
|
538
|
+
if (wordsRemaining === 0)
|
|
539
|
+
return segmentIndex === segments.length;
|
|
540
|
+
const segmentsRemaining = segments.length - segmentIndex;
|
|
541
|
+
if (segmentsRemaining < wordsRemaining)
|
|
542
|
+
return false;
|
|
543
|
+
for (let end = segmentIndex + 1; end <= segments.length - (wordsRemaining - 1); end += 1) {
|
|
544
|
+
const candidate = segments.slice(segmentIndex, end).join("-");
|
|
545
|
+
if (candidate.length > 10)
|
|
546
|
+
break;
|
|
547
|
+
if (isChoiceWord(candidate) && canSegmentChoiceWords(segments, end, wordsRemaining - 1)) {
|
|
548
|
+
return true;
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
return false;
|
|
552
|
+
}
|
|
553
|
+
function isServerStoredChoiceRecord(value) {
|
|
554
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
555
|
+
return false;
|
|
556
|
+
return ("v" in value &&
|
|
557
|
+
value.v === SERVER_STORED_CHOICE_RECORD_VERSION &&
|
|
558
|
+
"storage" in value &&
|
|
559
|
+
value.storage === "server" &&
|
|
560
|
+
"status" in value &&
|
|
561
|
+
(value.status === "active" || value.status === "consumed") &&
|
|
562
|
+
"provider_id" in value &&
|
|
563
|
+
typeof value.provider_id === "string" &&
|
|
564
|
+
"purpose" in value &&
|
|
565
|
+
typeof value.purpose === "string" &&
|
|
566
|
+
"issued_at_ms" in value &&
|
|
567
|
+
typeof value.issued_at_ms === "number" &&
|
|
568
|
+
"ttl_ms" in value &&
|
|
569
|
+
typeof value.ttl_ms === "number" &&
|
|
570
|
+
(!("binding" in value) || value.binding === undefined || isChoiceBinding(value.binding)) &&
|
|
571
|
+
"prefix" in value &&
|
|
572
|
+
typeof value.prefix === "string" &&
|
|
573
|
+
"payload" in value &&
|
|
574
|
+
isChoicePayload(value.payload) &&
|
|
575
|
+
"payload_digest" in value &&
|
|
576
|
+
typeof value.payload_digest === "string" &&
|
|
577
|
+
"replay_key" in value &&
|
|
578
|
+
typeof value.replay_key === "string");
|
|
579
|
+
}
|
|
580
|
+
function wordChoiceNotFoundError() {
|
|
581
|
+
return new ProviderChoiceTokenError("invalid_payload", WORD_CHOICE_NOT_FOUND_MESSAGE);
|
|
222
582
|
}
|
|
223
583
|
function resolveIssueStorage(storage, payload) {
|
|
224
584
|
if (!storage || storage.mode === "inline")
|
|
@@ -255,6 +615,10 @@ function resolveChoiceStateNamespace(options) {
|
|
|
255
615
|
function stateTtl(storage, ttlMs) {
|
|
256
616
|
return storage.ttl ?? `${ttlMs ?? 1}ms`;
|
|
257
617
|
}
|
|
618
|
+
function remainingStateTtl(expiresAt) {
|
|
619
|
+
const remainingMs = Date.parse(expiresAt) - Date.now();
|
|
620
|
+
return `${Number.isFinite(remainingMs) ? Math.max(1, Math.floor(remainingMs)) : 1}ms`;
|
|
621
|
+
}
|
|
258
622
|
function optionsStateKey(stateId) {
|
|
259
623
|
return stateId;
|
|
260
624
|
}
|
|
@@ -264,6 +628,9 @@ function serializeChoicePayload(payload) {
|
|
|
264
628
|
function digestChoicePayload(serializedPayload) {
|
|
265
629
|
return createHash("sha256").update(serializedPayload).digest("base64url");
|
|
266
630
|
}
|
|
631
|
+
function digestChoiceReplayKey(token) {
|
|
632
|
+
return createHash("sha256").update(token).digest("hex");
|
|
633
|
+
}
|
|
267
634
|
function isServerChoiceHandlePayload(value) {
|
|
268
635
|
return (value.storage === "server" &&
|
|
269
636
|
typeof value.state_id === "string" &&
|
|
@@ -366,6 +733,27 @@ function createChoiceBinding(options) {
|
|
|
366
733
|
...(credentialHash ? { credential_hash: credentialHash } : {}),
|
|
367
734
|
};
|
|
368
735
|
}
|
|
736
|
+
function hasRequestedChoiceBinding(options) {
|
|
737
|
+
return options?.connection === true || Boolean(options?.credentialKeys?.length);
|
|
738
|
+
}
|
|
739
|
+
function assertWordChoiceBindingMatches(options) {
|
|
740
|
+
const hasStoredBinding = Boolean(options.actual?.connection_hash || options.actual?.credential_hash);
|
|
741
|
+
const hasRequestedBinding = hasRequestedChoiceBinding(options.requested);
|
|
742
|
+
if (!hasStoredBinding && !hasRequestedBinding)
|
|
743
|
+
return;
|
|
744
|
+
if (hasStoredBinding !== hasRequestedBinding)
|
|
745
|
+
throw wordChoiceNotFoundError();
|
|
746
|
+
assertChoiceBindingMatches({
|
|
747
|
+
actual: options.actual,
|
|
748
|
+
expected: createChoiceBinding({
|
|
749
|
+
keys: options.resolveKeys(),
|
|
750
|
+
options: options.requested,
|
|
751
|
+
request: options.request,
|
|
752
|
+
credential: options.credential,
|
|
753
|
+
required: true,
|
|
754
|
+
}),
|
|
755
|
+
});
|
|
756
|
+
}
|
|
369
757
|
function hashRequiredConnection(options) {
|
|
370
758
|
const connectionId = options.request?.connectionId;
|
|
371
759
|
if (!connectionId) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { ProviderDefinition } from "../types.js";
|
|
2
2
|
export declare function isStreamingOperation(provider: ProviderDefinition, operationId: string): boolean;
|
|
3
3
|
/**
|
|
4
4
|
* Execute a provider operation by calling its handler.
|
|
@@ -11,6 +11,6 @@ export declare function isStreamingOperation(provider: ProviderDefinition, opera
|
|
|
11
11
|
*
|
|
12
12
|
* @see openspec/provider-sdk/03-sdk-core.md §3.6
|
|
13
13
|
*/
|
|
14
|
-
export declare function executeOperation(provider:
|
|
14
|
+
export declare function executeOperation<const TProvider extends ProviderDefinition, const TOperationId extends keyof TProvider["operations"] & string>(provider: TProvider, operationId: TOperationId, ctx: NoInfer<Parameters<TProvider["operations"][TOperationId]["handler"]>[0]>, input: unknown, _options?: {
|
|
15
15
|
skipAuth?: boolean;
|
|
16
16
|
}): Promise<unknown>;
|