@apifuse/provider-sdk 2.2.0-beta.30 → 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/CHANGELOG.md +4 -0
- package/dist/error-resolution.js +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/provider.d.ts +1 -1
- package/dist/provider.js +1 -1
- package/dist/runtime/choice.d.ts +1 -0
- package/dist/runtime/choice.js +96 -21
- package/dist/server/serve-implementation.js +2 -1
- package/package.json +1 -1
- package/src/error-resolution.ts +1 -0
- package/src/index.ts +1 -0
- package/src/provider.ts +1 -0
- package/src/runtime/choice.ts +133 -41
- package/src/server/serve-implementation.ts +2 -0
package/CHANGELOG.md
CHANGED
package/dist/error-resolution.js
CHANGED
|
@@ -18,6 +18,7 @@ export const SDK_OWNED_PROVIDER_ERROR_CODES = new Set([
|
|
|
18
18
|
"RUNTIME_UNSUPPORTED",
|
|
19
19
|
"PROVIDER_STATE_UNSUPPORTED",
|
|
20
20
|
"CHOICE_TOKEN_MASTER_SECRET_NOT_CONFIGURED",
|
|
21
|
+
"CHOICE_WORD_ISSUANCE_INVALID",
|
|
21
22
|
"CHOICE_STATE_PAYLOAD_TOO_LARGE",
|
|
22
23
|
"CHOICE_STATE_UNAVAILABLE",
|
|
23
24
|
"CHOICE_CONTEXT_REQUIRED",
|
package/dist/index.d.ts
CHANGED
|
@@ -17,7 +17,7 @@ export * from "./recipes/rest-api.js";
|
|
|
17
17
|
export { createFlowContext, createScratchpad } from "./runtime/auth-flow.js";
|
|
18
18
|
export type { BrowserClientOptions } from "./runtime/browser.js";
|
|
19
19
|
export { APIFUSE__CACHE__KEY_PEPPER_ENV, createBypassProviderCache, createProviderCache, type ProviderCacheOptions, } from "./runtime/cache.js";
|
|
20
|
-
export { type CreateProviderChoiceContextOptions, createProviderChoiceContext, createTestProviderChoiceContext, PROVIDER_RUNTIME_CHOICE_TOKEN_MASTER_SECRET_ENV, } from "./runtime/choice.js";
|
|
20
|
+
export { type CreateProviderChoiceContextOptions, createProviderChoiceContext, createTestProviderChoiceContext, PROVIDER_RUNTIME_CHOICE_TOKEN_MASTER_SECRET_ENV, PROVIDER_RUNTIME_CHOICE_WORD_ISSUANCE_ENV, } from "./runtime/choice.js";
|
|
21
21
|
export { type CreateCredentialContextOptions, createCredentialContext, } from "./runtime/credential.js";
|
|
22
22
|
export { createEnvContext } from "./runtime/env.js";
|
|
23
23
|
export { executeOperation } from "./runtime/executor.js";
|
package/dist/index.js
CHANGED
|
@@ -15,7 +15,7 @@ export * from "./recipes/gov-api.js";
|
|
|
15
15
|
export * from "./recipes/rest-api.js";
|
|
16
16
|
export { createFlowContext, createScratchpad } from "./runtime/auth-flow.js";
|
|
17
17
|
export { APIFUSE__CACHE__KEY_PEPPER_ENV, createBypassProviderCache, createProviderCache, } from "./runtime/cache.js";
|
|
18
|
-
export { createProviderChoiceContext, createTestProviderChoiceContext, PROVIDER_RUNTIME_CHOICE_TOKEN_MASTER_SECRET_ENV, } from "./runtime/choice.js";
|
|
18
|
+
export { createProviderChoiceContext, createTestProviderChoiceContext, PROVIDER_RUNTIME_CHOICE_TOKEN_MASTER_SECRET_ENV, PROVIDER_RUNTIME_CHOICE_WORD_ISSUANCE_ENV, } from "./runtime/choice.js";
|
|
19
19
|
export { createCredentialContext, } from "./runtime/credential.js";
|
|
20
20
|
export { createEnvContext } from "./runtime/env.js";
|
|
21
21
|
export { executeOperation } from "./runtime/executor.js";
|
package/dist/provider.d.ts
CHANGED
|
@@ -5,7 +5,7 @@ export { assertFreshProviderChoiceIssuedAt, createProviderChoiceToken, ProviderC
|
|
|
5
5
|
export { centered, delayed, defineHealthJourney, defineOperation, defineProvider, defineSmsOtpMatcher, every, } from "./define.js";
|
|
6
6
|
export { AuthError, HttpRedirectError, isProviderError, isSessionExpiredError, isTransportError, ProviderError, SessionExpiredError, TransportError, ValidationError, } from "./errors.js";
|
|
7
7
|
export { getProviderLocalePath, providerLocaleKey, qualifyProviderLocaleKey, } from "./i18n/index.js";
|
|
8
|
-
export { type CreateProviderChoiceContextOptions, createProviderChoiceContext, createTestProviderChoiceContext, PROVIDER_RUNTIME_CHOICE_TOKEN_MASTER_SECRET_ENV, } from "./runtime/choice.js";
|
|
8
|
+
export { type CreateProviderChoiceContextOptions, createProviderChoiceContext, createTestProviderChoiceContext, PROVIDER_RUNTIME_CHOICE_TOKEN_MASTER_SECRET_ENV, PROVIDER_RUNTIME_CHOICE_WORD_ISSUANCE_ENV, } from "./runtime/choice.js";
|
|
9
9
|
export { APIFUSE_DESCRIPTION_KEY_META_KEY, APIFUSE_REDACTION_MARKER, APIFUSE_SENSITIVE_KIND_META_KEY, APIFUSE_SENSITIVE_META_KEY, collectSensitivePaths, describeKey, field, fields, isSensitiveSchema, redactPayload, type SensitiveFieldKind, type SensitiveFieldOptions, type SensitivePath, sensitive, z, } from "./schema.js";
|
|
10
10
|
export type { AuthAbortData, AuthAbortRetry, AuthFlowTerminalContext, AuthMode, AuthSafeData, AuthSafeJson, FlowContext, HealthCheckAssertionContext, HealthCheckCase, HealthCheckSuite, HealthCheckUnsupported, HealthJourneyDefinition, HealthJourneyEventContext, HealthJourneyManualTriggerPolicy, HealthJourneyRunContext, HealthJourneyRunResult, HealthScheduleRandomization, HttpRedirectFailureReason, HttpRedirectPolicy, HttpRedirectPolicyMode, HttpRetryOptions, HttpRetrySummary, InferSchemaOutput, NativeContext, NativeNetworkClient, NativeNetworkCloseReason, NativeNetworkConnection, NativeNetworkConnectInput, NativeNetworkConnectOptions, NativeNetworkDynamicGrantOptions, NativeNetworkEgressGrant, NativeProviderConfig, NativeProviderContext, NativeProxyDrainHandler, NativeProxyEgressInfo, NativeProxyExpiringEvent, NativeProxyExpiringReason, NativeTcpDynamicEgressRule, NativeTcpEgressGrant, NativeTcpEgressRule, NativeTcpPortRange, NativeTcpTlsMode, NativeTlsConnectOptions, OperationApprovalPolicy, OperationContractMetadata, OperationDefinition, OperationDocMeta, OperationErrorCode, ProviderErrorStatus, OperationInputExample, OperationLifecycle, OperationObservabilityConfig, OperationObservabilitySensitiveConfig, OperationRelationships, OperationRiskClass, OperationSensitivePath, OperationToolRouterMetadata, OperationTransport, ProviderAccessVisibility, ProviderChoiceBindingOptions, ProviderChoiceConsumeMode, ProviderChoiceConsumeResult, ProviderChoiceContext, ProviderChoiceExplicitParseResult, ProviderChoiceIssueOptions, ProviderChoiceParseOptions, ProviderContext, ProviderDefinition, ProviderDeploymentOverrides, ProviderFileRef, ProviderFilesContext, ProviderLocale, ProviderLocaleKey, ProviderLocaleKeyInput, ProviderLogoProfile, ProviderProxyPolicy, ProxiedOAuthConfig, ProviderPublicConnectionMode, ProviderPublicProfile, ProviderResolvedFile, ProviderRuntimeState, ProviderStateDurationString, ProviderStateNamespace, ProviderSupportLevel, RedirectRunReason, SchemaLike, SmsOtpMatcherDefinition, StandardSchemaV1, StateCasResult, StateNamespaceOptions, StateNamespaceScope, StateValue, StateWriteOptions, } from "./types.js";
|
|
11
11
|
export { NativeEgressGrantExpiredError, NativeEgressNotDeclaredError, NativeIdleTimeoutError, NativeNetworkError, NativeProxyExpiredError, } from "./runtime/native-network-errors.js";
|
package/dist/provider.js
CHANGED
|
@@ -4,7 +4,7 @@ export { assertFreshProviderChoiceIssuedAt, createProviderChoiceToken, ProviderC
|
|
|
4
4
|
export { centered, delayed, defineHealthJourney, defineOperation, defineProvider, defineSmsOtpMatcher, every, } from "./define.js";
|
|
5
5
|
export { AuthError, HttpRedirectError, isProviderError, isSessionExpiredError, isTransportError, ProviderError, SessionExpiredError, TransportError, ValidationError, } from "./errors.js";
|
|
6
6
|
export { getProviderLocalePath, providerLocaleKey, qualifyProviderLocaleKey, } from "./i18n/index.js";
|
|
7
|
-
export { createProviderChoiceContext, createTestProviderChoiceContext, PROVIDER_RUNTIME_CHOICE_TOKEN_MASTER_SECRET_ENV, } from "./runtime/choice.js";
|
|
7
|
+
export { createProviderChoiceContext, createTestProviderChoiceContext, PROVIDER_RUNTIME_CHOICE_TOKEN_MASTER_SECRET_ENV, PROVIDER_RUNTIME_CHOICE_WORD_ISSUANCE_ENV, } from "./runtime/choice.js";
|
|
8
8
|
export { APIFUSE_DESCRIPTION_KEY_META_KEY, APIFUSE_REDACTION_MARKER, APIFUSE_SENSITIVE_KIND_META_KEY, APIFUSE_SENSITIVE_META_KEY, collectSensitivePaths, describeKey, field, fields, isSensitiveSchema, redactPayload, sensitive, z, } from "./schema.js";
|
|
9
9
|
export { NativeEgressGrantExpiredError, NativeEgressNotDeclaredError, NativeIdleTimeoutError, NativeNetworkError, NativeProxyExpiredError, } from "./runtime/native-network-errors.js";
|
|
10
10
|
export { HttpRetryAfterPolicy, HttpRetryDelayStrategy, HttpRetryJitter, HttpRetryPreset, HttpRetryUnsafeMethodPolicy, } from "./types.js";
|
package/dist/runtime/choice.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { CredentialContext, EnvContext, ProviderChoiceConsumeMode, ProviderChoiceContext, ProviderRequestContext, ProviderRuntimeState } from "../types.js";
|
|
2
2
|
export declare const PROVIDER_RUNTIME_CHOICE_TOKEN_MASTER_SECRET_ENV = "APIFUSE__PROVIDER_RUNTIME__CHOICE_TOKEN_MASTER_SECRET";
|
|
3
|
+
export declare const PROVIDER_RUNTIME_CHOICE_WORD_ISSUANCE_ENV = "APIFUSE__PROVIDER_RUNTIME__CHOICE_WORD_ISSUANCE";
|
|
3
4
|
export type ProviderChoiceTelemetryEvent = {
|
|
4
5
|
readonly providerId: string;
|
|
5
6
|
readonly purpose: string;
|
package/dist/runtime/choice.js
CHANGED
|
@@ -3,6 +3,7 @@ import { assertFreshProviderChoiceIssuedAt, ProviderChoiceTokenError, } from "..
|
|
|
3
3
|
import { isProviderError, ProviderError } from "../errors.js";
|
|
4
4
|
import { CHOICE_WORDLIST_SIZE, choiceWordAt, HIGH_CHOICE_WORD_COUNT, isChoiceWord, STANDARD_CHOICE_WORD_COUNT, } from "./choice-wordlist.js";
|
|
5
5
|
export const PROVIDER_RUNTIME_CHOICE_TOKEN_MASTER_SECRET_ENV = "APIFUSE__PROVIDER_RUNTIME__CHOICE_TOKEN_MASTER_SECRET";
|
|
6
|
+
export const PROVIDER_RUNTIME_CHOICE_WORD_ISSUANCE_ENV = "APIFUSE__PROVIDER_RUNTIME__CHOICE_WORD_ISSUANCE";
|
|
6
7
|
const PRIMARY_CHOICE_TOKEN_KID = "v1";
|
|
7
8
|
const MANAGED_CHOICE_TOKEN_VERSION = 1;
|
|
8
9
|
const SERVER_STORED_CHOICE_RECORD_VERSION = 1;
|
|
@@ -15,28 +16,53 @@ export function createProviderChoiceContext(options) {
|
|
|
15
16
|
const issuedAtMs = issueOptions.nowMs ?? Date.now();
|
|
16
17
|
const resolvedStorage = resolveIssueStorage(issueOptions.storage, issueOptions.payload);
|
|
17
18
|
if (resolvedStorage.mode === "server") {
|
|
19
|
+
const issuance = resolveChoiceWordIssuance(options.env);
|
|
20
|
+
const keys = hasRequestedChoiceBinding(issueOptions.bind)
|
|
21
|
+
? deriveManagedChoiceKeys({
|
|
22
|
+
masterSecret: resolveMasterSecret(),
|
|
23
|
+
providerId: options.providerId,
|
|
24
|
+
purpose: issueOptions.purpose,
|
|
25
|
+
kid,
|
|
26
|
+
})
|
|
27
|
+
: undefined;
|
|
18
28
|
const binding = hasRequestedChoiceBinding(issueOptions.bind)
|
|
19
29
|
? createChoiceBinding({
|
|
20
|
-
keys:
|
|
21
|
-
masterSecret: resolveMasterSecret(),
|
|
22
|
-
providerId: options.providerId,
|
|
23
|
-
purpose: issueOptions.purpose,
|
|
24
|
-
kid,
|
|
25
|
-
}),
|
|
30
|
+
keys: keys,
|
|
26
31
|
options: issueOptions.bind,
|
|
27
32
|
request: options.request,
|
|
28
33
|
credential: options.credential,
|
|
29
34
|
required: true,
|
|
30
35
|
})
|
|
31
36
|
: undefined;
|
|
32
|
-
|
|
37
|
+
const baseEnvelope = {
|
|
38
|
+
v: MANAGED_CHOICE_TOKEN_VERSION,
|
|
39
|
+
provider_id: options.providerId,
|
|
40
|
+
purpose: issueOptions.purpose,
|
|
41
|
+
issued_at_ms: issuedAtMs,
|
|
42
|
+
ttl_ms: issueOptions.ttlMs,
|
|
43
|
+
binding,
|
|
44
|
+
};
|
|
45
|
+
if (issuance === "legacy") {
|
|
46
|
+
const legacyKeys = keys ??
|
|
47
|
+
deriveManagedChoiceKeys({
|
|
48
|
+
masterSecret: resolveMasterSecret(),
|
|
49
|
+
providerId: options.providerId,
|
|
50
|
+
purpose: issueOptions.purpose,
|
|
51
|
+
kid,
|
|
52
|
+
});
|
|
53
|
+
return issueLegacyServerStoredChoice({
|
|
54
|
+
baseEnvelope,
|
|
55
|
+
issueOptions,
|
|
56
|
+
storage: resolvedStorage.storage,
|
|
57
|
+
contextState: options.state,
|
|
58
|
+
kid,
|
|
59
|
+
keys: legacyKeys,
|
|
60
|
+
issuedAtMs,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
return issueWordServerStoredChoice({
|
|
33
64
|
baseEnvelope: {
|
|
34
|
-
|
|
35
|
-
provider_id: options.providerId,
|
|
36
|
-
purpose: issueOptions.purpose,
|
|
37
|
-
issued_at_ms: issuedAtMs,
|
|
38
|
-
ttl_ms: issueOptions.ttlMs,
|
|
39
|
-
binding,
|
|
65
|
+
...baseEnvelope,
|
|
40
66
|
},
|
|
41
67
|
issueOptions,
|
|
42
68
|
storage: resolvedStorage.storage,
|
|
@@ -133,13 +159,7 @@ export function createProviderChoiceContext(options) {
|
|
|
133
159
|
purpose: parseOptions.purpose,
|
|
134
160
|
kid: tokenKid,
|
|
135
161
|
});
|
|
136
|
-
const signedBody = [
|
|
137
|
-
parseOptions.prefix,
|
|
138
|
-
tokenKid,
|
|
139
|
-
encodedIv,
|
|
140
|
-
encryptedPayload,
|
|
141
|
-
authTag,
|
|
142
|
-
].join(".");
|
|
162
|
+
const signedBody = [parseOptions.prefix, tokenKid, encodedIv, encryptedPayload, authTag].join(".");
|
|
143
163
|
assertManagedChoiceSignature({
|
|
144
164
|
signedBody,
|
|
145
165
|
signature,
|
|
@@ -287,6 +307,20 @@ function createLegacyExplicitParseResult(options) {
|
|
|
287
307
|
},
|
|
288
308
|
};
|
|
289
309
|
}
|
|
310
|
+
function resolveChoiceWordIssuance(env) {
|
|
311
|
+
const configured = env?.get(PROVIDER_RUNTIME_CHOICE_WORD_ISSUANCE_ENV);
|
|
312
|
+
const value = configured?.trim() ?? "";
|
|
313
|
+
if (value === "" || value === "legacy")
|
|
314
|
+
return "legacy";
|
|
315
|
+
if (value === "word")
|
|
316
|
+
return "word";
|
|
317
|
+
throw new ProviderError(`Unsupported provider choice word issuance mode "${value}". Expected "legacy" or "word".`, {
|
|
318
|
+
code: "CHOICE_WORD_ISSUANCE_INVALID",
|
|
319
|
+
category: "input_validation",
|
|
320
|
+
retryable: false,
|
|
321
|
+
details: { env: PROVIDER_RUNTIME_CHOICE_WORD_ISSUANCE_ENV },
|
|
322
|
+
});
|
|
323
|
+
}
|
|
290
324
|
function resolveChoiceMasterSecret(options) {
|
|
291
325
|
const configured = options.masterSecret ?? options.env?.get(PROVIDER_RUNTIME_CHOICE_TOKEN_MASTER_SECRET_ENV);
|
|
292
326
|
const trimmed = configured?.trim();
|
|
@@ -336,7 +370,7 @@ function encryptManagedChoiceToken(options) {
|
|
|
336
370
|
.digest("base64url");
|
|
337
371
|
return `${signedBody}.${signature}`;
|
|
338
372
|
}
|
|
339
|
-
async function
|
|
373
|
+
async function issueWordServerStoredChoice(options) {
|
|
340
374
|
const serializedPayload = serializeChoicePayload(options.issueOptions.payload);
|
|
341
375
|
const payloadDigest = digestChoicePayload(serializedPayload);
|
|
342
376
|
const namespace = resolveChoiceStateNamespace({
|
|
@@ -386,6 +420,47 @@ async function issueServerStoredChoice(options) {
|
|
|
386
420
|
retryable: false,
|
|
387
421
|
});
|
|
388
422
|
}
|
|
423
|
+
/**
|
|
424
|
+
* Beta.28-compatible server handle issuance. The state stores the payload and
|
|
425
|
+
* the client receives the existing six-part encrypted envelope whose payload
|
|
426
|
+
* is a server-state handle.
|
|
427
|
+
*/
|
|
428
|
+
async function issueLegacyServerStoredChoice(options) {
|
|
429
|
+
const serializedPayload = serializeChoicePayload(options.issueOptions.payload);
|
|
430
|
+
const payloadBytes = Buffer.byteLength(serializedPayload, "utf8");
|
|
431
|
+
if (payloadBytes > options.storage.maxValueBytes) {
|
|
432
|
+
throw new ProviderError("Provider choice payload exceeds state storage policy.", {
|
|
433
|
+
code: "CHOICE_STATE_PAYLOAD_TOO_LARGE",
|
|
434
|
+
category: "input_validation",
|
|
435
|
+
retryable: false,
|
|
436
|
+
details: { maxValueBytes: options.storage.maxValueBytes, payloadBytes },
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
const stateId = `choice_${randomBytes(16).toString("base64url")}`;
|
|
440
|
+
const namespace = resolveChoiceStateNamespace({
|
|
441
|
+
storage: options.storage,
|
|
442
|
+
contextState: options.contextState,
|
|
443
|
+
ttlMs: options.issueOptions.ttlMs,
|
|
444
|
+
});
|
|
445
|
+
await namespace.set(optionsStateKey(stateId), options.issueOptions.payload, {
|
|
446
|
+
ttl: stateTtl(options.storage, options.issueOptions.ttlMs),
|
|
447
|
+
});
|
|
448
|
+
const envelope = {
|
|
449
|
+
...options.baseEnvelope,
|
|
450
|
+
payload: {
|
|
451
|
+
storage: "server",
|
|
452
|
+
state_id: stateId,
|
|
453
|
+
payload_digest: digestChoicePayload(serializedPayload),
|
|
454
|
+
created_at_ms: options.issuedAtMs,
|
|
455
|
+
},
|
|
456
|
+
};
|
|
457
|
+
return encryptManagedChoiceToken({
|
|
458
|
+
prefix: options.issueOptions.prefix,
|
|
459
|
+
kid: options.kid,
|
|
460
|
+
envelope,
|
|
461
|
+
keys: options.keys,
|
|
462
|
+
});
|
|
463
|
+
}
|
|
389
464
|
async function parseWordServerStoredChoice(options) {
|
|
390
465
|
const storage = resolveParseStorage(options.parseOptions.storage);
|
|
391
466
|
const namespace = resolveChoiceStateNamespace({
|
|
@@ -11,7 +11,7 @@ import { loadProviderLocaleCatalogs, localizeAuthTurn, } from "../i18n/catalog.j
|
|
|
11
11
|
import { categoryForStatus, sourceForCategory, isRetryableCategory, PROVIDER_OBSERVABILITY_TAXONOMY_VERSION, } from "../observability.js";
|
|
12
12
|
import { createScratchpad } from "../runtime/auth-flow.js";
|
|
13
13
|
import { createProviderCache } from "../runtime/cache.js";
|
|
14
|
-
import { createProviderChoiceContext, PROVIDER_RUNTIME_CHOICE_TOKEN_MASTER_SECRET_ENV, } from "../runtime/choice.js";
|
|
14
|
+
import { createProviderChoiceContext, PROVIDER_RUNTIME_CHOICE_TOKEN_MASTER_SECRET_ENV, PROVIDER_RUNTIME_CHOICE_WORD_ISSUANCE_ENV, } from "../runtime/choice.js";
|
|
15
15
|
import { createCredentialContext } from "../runtime/credential.js";
|
|
16
16
|
import { createEnvContext } from "../runtime/env.js";
|
|
17
17
|
import { executeOperation } from "../runtime/executor.js";
|
|
@@ -376,6 +376,7 @@ function createProviderContext(provider, request, operationId, options, state =
|
|
|
376
376
|
const env = createEnvContext([
|
|
377
377
|
...(provider.secrets?.map((secret) => secret.name) ?? []),
|
|
378
378
|
PROVIDER_RUNTIME_CHOICE_TOKEN_MASTER_SECRET_ENV,
|
|
379
|
+
PROVIDER_RUNTIME_CHOICE_WORD_ISSUANCE_ENV,
|
|
379
380
|
]);
|
|
380
381
|
const credential = createCredentialContext({
|
|
381
382
|
allowedKeys: provider.credential?.keys,
|
package/package.json
CHANGED
package/src/error-resolution.ts
CHANGED
|
@@ -20,6 +20,7 @@ export const SDK_OWNED_PROVIDER_ERROR_CODES = new Set([
|
|
|
20
20
|
"RUNTIME_UNSUPPORTED",
|
|
21
21
|
"PROVIDER_STATE_UNSUPPORTED",
|
|
22
22
|
"CHOICE_TOKEN_MASTER_SECRET_NOT_CONFIGURED",
|
|
23
|
+
"CHOICE_WORD_ISSUANCE_INVALID",
|
|
23
24
|
"CHOICE_STATE_PAYLOAD_TOO_LARGE",
|
|
24
25
|
"CHOICE_STATE_UNAVAILABLE",
|
|
25
26
|
"CHOICE_CONTEXT_REQUIRED",
|
package/src/index.ts
CHANGED
package/src/provider.ts
CHANGED
package/src/runtime/choice.ts
CHANGED
|
@@ -39,6 +39,8 @@ import type {
|
|
|
39
39
|
|
|
40
40
|
export const PROVIDER_RUNTIME_CHOICE_TOKEN_MASTER_SECRET_ENV =
|
|
41
41
|
"APIFUSE__PROVIDER_RUNTIME__CHOICE_TOKEN_MASTER_SECRET";
|
|
42
|
+
export const PROVIDER_RUNTIME_CHOICE_WORD_ISSUANCE_ENV =
|
|
43
|
+
"APIFUSE__PROVIDER_RUNTIME__CHOICE_WORD_ISSUANCE";
|
|
42
44
|
|
|
43
45
|
const PRIMARY_CHOICE_TOKEN_KID = "v1";
|
|
44
46
|
const MANAGED_CHOICE_TOKEN_VERSION = 1;
|
|
@@ -131,28 +133,54 @@ export function createProviderChoiceContext(
|
|
|
131
133
|
const issuedAtMs = issueOptions.nowMs ?? Date.now();
|
|
132
134
|
const resolvedStorage = resolveIssueStorage(issueOptions.storage, issueOptions.payload);
|
|
133
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;
|
|
134
145
|
const binding = hasRequestedChoiceBinding(issueOptions.bind)
|
|
135
146
|
? createChoiceBinding({
|
|
136
|
-
keys:
|
|
137
|
-
masterSecret: resolveMasterSecret(),
|
|
138
|
-
providerId: options.providerId,
|
|
139
|
-
purpose: issueOptions.purpose,
|
|
140
|
-
kid,
|
|
141
|
-
}),
|
|
147
|
+
keys: keys!,
|
|
142
148
|
options: issueOptions.bind,
|
|
143
149
|
request: options.request,
|
|
144
150
|
credential: options.credential,
|
|
145
151
|
required: true,
|
|
146
152
|
})
|
|
147
153
|
: undefined;
|
|
148
|
-
|
|
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({
|
|
149
182
|
baseEnvelope: {
|
|
150
|
-
|
|
151
|
-
provider_id: options.providerId,
|
|
152
|
-
purpose: issueOptions.purpose,
|
|
153
|
-
issued_at_ms: issuedAtMs,
|
|
154
|
-
ttl_ms: issueOptions.ttlMs,
|
|
155
|
-
binding,
|
|
183
|
+
...baseEnvelope,
|
|
156
184
|
},
|
|
157
185
|
issueOptions,
|
|
158
186
|
storage: resolvedStorage.storage,
|
|
@@ -233,8 +261,8 @@ export function createProviderChoiceContext(
|
|
|
233
261
|
masterSecret: resolveMasterSecret(),
|
|
234
262
|
providerId: options.providerId,
|
|
235
263
|
purpose: parseOptions.purpose,
|
|
236
|
-
|
|
237
|
-
|
|
264
|
+
kid,
|
|
265
|
+
}),
|
|
238
266
|
onConsume: (result) =>
|
|
239
267
|
emitChoiceTelemetry(options.onTelemetry, {
|
|
240
268
|
providerId: options.providerId,
|
|
@@ -247,15 +275,16 @@ export function createProviderChoiceContext(
|
|
|
247
275
|
replay: result.status === "already-consumed",
|
|
248
276
|
}),
|
|
249
277
|
});
|
|
250
|
-
return observeChoiceParse<
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
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
|
+
);
|
|
259
288
|
}
|
|
260
289
|
|
|
261
290
|
// Legacy encrypted-envelope compatibility fallback. Removal is gated on
|
|
@@ -285,13 +314,9 @@ export function createProviderChoiceContext(
|
|
|
285
314
|
purpose: parseOptions.purpose,
|
|
286
315
|
kid: tokenKid,
|
|
287
316
|
});
|
|
288
|
-
const signedBody = [
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
encodedIv,
|
|
292
|
-
encryptedPayload,
|
|
293
|
-
authTag,
|
|
294
|
-
].join(".");
|
|
317
|
+
const signedBody = [parseOptions.prefix, tokenKid, encodedIv, encryptedPayload, authTag].join(
|
|
318
|
+
".",
|
|
319
|
+
);
|
|
295
320
|
assertManagedChoiceSignature({
|
|
296
321
|
signedBody,
|
|
297
322
|
signature,
|
|
@@ -347,15 +372,16 @@ export function createProviderChoiceContext(
|
|
|
347
372
|
}),
|
|
348
373
|
)
|
|
349
374
|
: payload;
|
|
350
|
-
return observeChoiceParse<
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
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
|
+
);
|
|
359
385
|
} catch (error) {
|
|
360
386
|
emitChoiceParseFailure(options.onTelemetry, error, {
|
|
361
387
|
providerId: options.providerId,
|
|
@@ -485,6 +511,22 @@ function createLegacyExplicitParseResult(options: {
|
|
|
485
511
|
};
|
|
486
512
|
}
|
|
487
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";
|
|
519
|
+
throw new ProviderError(
|
|
520
|
+
`Unsupported provider choice word issuance mode "${value}". Expected "legacy" or "word".`,
|
|
521
|
+
{
|
|
522
|
+
code: "CHOICE_WORD_ISSUANCE_INVALID",
|
|
523
|
+
category: "input_validation",
|
|
524
|
+
retryable: false,
|
|
525
|
+
details: { env: PROVIDER_RUNTIME_CHOICE_WORD_ISSUANCE_ENV },
|
|
526
|
+
},
|
|
527
|
+
);
|
|
528
|
+
}
|
|
529
|
+
|
|
488
530
|
function resolveChoiceMasterSecret(options: CreateProviderChoiceContextOptions): string {
|
|
489
531
|
const configured =
|
|
490
532
|
options.masterSecret ?? options.env?.get(PROVIDER_RUNTIME_CHOICE_TOKEN_MASTER_SECRET_ENV);
|
|
@@ -559,7 +601,7 @@ function encryptManagedChoiceToken(options: {
|
|
|
559
601
|
return `${signedBody}.${signature}`;
|
|
560
602
|
}
|
|
561
603
|
|
|
562
|
-
async function
|
|
604
|
+
async function issueWordServerStoredChoice<TPayload extends ProviderChoiceTokenPayload>(options: {
|
|
563
605
|
readonly baseEnvelope: Omit<ManagedChoiceEnvelope, "payload">;
|
|
564
606
|
readonly issueOptions: ProviderChoiceIssueOptions<TPayload>;
|
|
565
607
|
readonly storage: ServerProviderChoiceStorageOptions;
|
|
@@ -615,6 +657,56 @@ async function issueServerStoredChoice<TPayload extends ProviderChoiceTokenPaylo
|
|
|
615
657
|
});
|
|
616
658
|
}
|
|
617
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
|
+
});
|
|
683
|
+
}
|
|
684
|
+
const stateId = `choice_${randomBytes(16).toString("base64url")}`;
|
|
685
|
+
const namespace = resolveChoiceStateNamespace({
|
|
686
|
+
storage: options.storage,
|
|
687
|
+
contextState: options.contextState,
|
|
688
|
+
ttlMs: options.issueOptions.ttlMs,
|
|
689
|
+
});
|
|
690
|
+
await namespace.set(optionsStateKey(stateId), options.issueOptions.payload, {
|
|
691
|
+
ttl: stateTtl(options.storage, options.issueOptions.ttlMs),
|
|
692
|
+
});
|
|
693
|
+
const envelope: ManagedChoiceEnvelope = {
|
|
694
|
+
...options.baseEnvelope,
|
|
695
|
+
payload: {
|
|
696
|
+
storage: "server",
|
|
697
|
+
state_id: stateId,
|
|
698
|
+
payload_digest: digestChoicePayload(serializedPayload),
|
|
699
|
+
created_at_ms: options.issuedAtMs,
|
|
700
|
+
},
|
|
701
|
+
};
|
|
702
|
+
return encryptManagedChoiceToken({
|
|
703
|
+
prefix: options.issueOptions.prefix,
|
|
704
|
+
kid: options.kid,
|
|
705
|
+
envelope,
|
|
706
|
+
keys: options.keys,
|
|
707
|
+
});
|
|
708
|
+
}
|
|
709
|
+
|
|
618
710
|
async function parseWordServerStoredChoice(options: {
|
|
619
711
|
readonly stateKey: string;
|
|
620
712
|
readonly parseOptions: ProviderChoiceParseOptions;
|
|
@@ -39,6 +39,7 @@ import { createProviderCache } from "../runtime/cache.js";
|
|
|
39
39
|
import {
|
|
40
40
|
createProviderChoiceContext,
|
|
41
41
|
PROVIDER_RUNTIME_CHOICE_TOKEN_MASTER_SECRET_ENV,
|
|
42
|
+
PROVIDER_RUNTIME_CHOICE_WORD_ISSUANCE_ENV,
|
|
42
43
|
} from "../runtime/choice.js";
|
|
43
44
|
import { createCredentialContext } from "../runtime/credential.js";
|
|
44
45
|
import { createEnvContext } from "../runtime/env.js";
|
|
@@ -626,6 +627,7 @@ function createProviderContext(
|
|
|
626
627
|
const env = createEnvContext([
|
|
627
628
|
...(provider.secrets?.map((secret) => secret.name) ?? []),
|
|
628
629
|
PROVIDER_RUNTIME_CHOICE_TOKEN_MASTER_SECRET_ENV,
|
|
630
|
+
PROVIDER_RUNTIME_CHOICE_WORD_ISSUANCE_ENV,
|
|
629
631
|
]);
|
|
630
632
|
const credential = createCredentialContext({
|
|
631
633
|
allowedKeys: provider.credential?.keys,
|