@apifuse/provider-sdk 2.1.0-beta.1 → 2.1.0-beta.10

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.
Files changed (212) hide show
  1. package/AUTHORING.md +208 -2
  2. package/CHANGELOG.md +47 -0
  3. package/README.md +114 -10
  4. package/SUBMISSION.md +87 -0
  5. package/bin/apifuse-check.ts +86 -4
  6. package/bin/apifuse-dev.ts +87 -13
  7. package/bin/apifuse-pack-check.ts +80 -0
  8. package/bin/apifuse-pack-smoke.ts +303 -2
  9. package/bin/apifuse-perf.ts +142 -49
  10. package/bin/apifuse-record.ts +182 -104
  11. package/bin/apifuse-submit-check.ts +2538 -0
  12. package/bin/apifuse.ts +1 -1
  13. package/dist/ceremonies/index.d.ts +41 -0
  14. package/dist/ceremonies/index.js +490 -0
  15. package/dist/choice-token.d.ts +24 -0
  16. package/dist/choice-token.js +74 -0
  17. package/dist/cli/commands.d.ts +10 -0
  18. package/dist/cli/commands.js +80 -0
  19. package/dist/cli/create.d.ts +47 -0
  20. package/dist/cli/create.js +762 -0
  21. package/dist/cli/templates/provider/.dockerignore.tpl +22 -0
  22. package/dist/cli/templates/provider/.gitignore.tpl +22 -0
  23. package/dist/cli/templates/provider/Dockerfile.tpl +7 -0
  24. package/dist/cli/templates/provider/README.md.tpl +160 -0
  25. package/dist/cli/templates/provider/dev.ts.tpl +5 -0
  26. package/dist/cli/templates/provider/domain/README.md.tpl +3 -0
  27. package/dist/cli/templates/provider/index.test.ts.tpl +13 -0
  28. package/dist/cli/templates/provider/index.ts.tpl +15 -0
  29. package/dist/cli/templates/provider/mappers/README.md.tpl +3 -0
  30. package/dist/cli/templates/provider/meta.ts.tpl +7 -0
  31. package/dist/cli/templates/provider/operations/index.ts.tpl +5 -0
  32. package/dist/cli/templates/provider/operations/ping.ts.tpl +24 -0
  33. package/dist/cli/templates/provider/schemas/ping.ts.tpl +24 -0
  34. package/dist/cli/templates/provider/start.ts.tpl +5 -0
  35. package/dist/cli/templates/provider/upstream/README.md.tpl +3 -0
  36. package/dist/config/loader.d.ts +107 -0
  37. package/dist/config/loader.js +935 -0
  38. package/dist/contract-json.d.ts +9 -0
  39. package/dist/contract-json.js +51 -0
  40. package/dist/contract-serialization.d.ts +4 -0
  41. package/dist/contract-serialization.js +78 -0
  42. package/dist/contract-types.d.ts +49 -0
  43. package/dist/contract-types.js +1 -0
  44. package/dist/contract.d.ts +6 -0
  45. package/dist/contract.js +155 -0
  46. package/dist/define.d.ts +97 -0
  47. package/dist/define.js +1320 -0
  48. package/dist/dev.d.ts +9 -0
  49. package/dist/dev.js +15 -0
  50. package/dist/errors.d.ts +59 -0
  51. package/dist/errors.js +97 -0
  52. package/dist/i18n/catalog.d.ts +29 -0
  53. package/dist/i18n/catalog.js +159 -0
  54. package/dist/i18n/index.d.ts +2 -0
  55. package/dist/i18n/index.js +2 -0
  56. package/dist/i18n/keys.d.ts +10 -0
  57. package/dist/i18n/keys.js +34 -0
  58. package/dist/index.d.ts +41 -0
  59. package/dist/index.js +37 -0
  60. package/dist/lint.d.ts +73 -0
  61. package/dist/lint.js +702 -0
  62. package/dist/observability.d.ts +5 -0
  63. package/dist/observability.js +39 -0
  64. package/dist/provider.d.ts +9 -0
  65. package/dist/provider.js +8 -0
  66. package/dist/public-schema-field-lint.d.ts +2 -0
  67. package/dist/public-schema-field-lint.js +158 -0
  68. package/dist/recipes/gov-api.d.ts +19 -0
  69. package/dist/recipes/gov-api.js +72 -0
  70. package/dist/recipes/rest-api.d.ts +21 -0
  71. package/dist/recipes/rest-api.js +115 -0
  72. package/dist/runtime/auth-flow.d.ts +14 -0
  73. package/dist/runtime/auth-flow.js +44 -0
  74. package/dist/runtime/browser.d.ts +25 -0
  75. package/dist/runtime/browser.js +1034 -0
  76. package/dist/runtime/cache.d.ts +10 -0
  77. package/dist/runtime/cache.js +372 -0
  78. package/dist/runtime/choice.d.ts +15 -0
  79. package/dist/runtime/choice.js +435 -0
  80. package/dist/runtime/credential.d.ts +8 -0
  81. package/dist/runtime/credential.js +61 -0
  82. package/dist/runtime/env.d.ts +2 -0
  83. package/dist/runtime/env.js +10 -0
  84. package/dist/runtime/executor.d.ts +16 -0
  85. package/dist/runtime/executor.js +51 -0
  86. package/dist/runtime/http.d.ts +8 -0
  87. package/dist/runtime/http.js +706 -0
  88. package/dist/runtime/insights.d.ts +9 -0
  89. package/dist/runtime/insights.js +324 -0
  90. package/dist/runtime/instrumentation.d.ts +8 -0
  91. package/dist/runtime/instrumentation.js +269 -0
  92. package/dist/runtime/key-derivation.d.ts +24 -0
  93. package/dist/runtime/key-derivation.js +73 -0
  94. package/dist/runtime/keyring.d.ts +25 -0
  95. package/dist/runtime/keyring.js +93 -0
  96. package/dist/runtime/namespace.d.ts +9 -0
  97. package/dist/runtime/namespace.js +19 -0
  98. package/dist/runtime/otlp.d.ts +39 -0
  99. package/dist/runtime/otlp.js +103 -0
  100. package/dist/runtime/perf.d.ts +12 -0
  101. package/dist/runtime/perf.js +52 -0
  102. package/dist/runtime/prevalidate.d.ts +12 -0
  103. package/dist/runtime/prevalidate.js +173 -0
  104. package/dist/runtime/provider.d.ts +2 -0
  105. package/dist/runtime/provider.js +11 -0
  106. package/dist/runtime/proxy-errors.d.ts +21 -0
  107. package/dist/runtime/proxy-errors.js +83 -0
  108. package/dist/runtime/proxy-telemetry.d.ts +8 -0
  109. package/dist/runtime/proxy-telemetry.js +174 -0
  110. package/dist/runtime/redis.d.ts +17 -0
  111. package/dist/runtime/redis.js +82 -0
  112. package/dist/runtime/request-options.d.ts +3 -0
  113. package/dist/runtime/request-options.js +42 -0
  114. package/dist/runtime/state.d.ts +17 -0
  115. package/dist/runtime/state.js +344 -0
  116. package/dist/runtime/stealth.d.ts +18 -0
  117. package/dist/runtime/stealth.js +834 -0
  118. package/dist/runtime/stt.d.ts +22 -0
  119. package/dist/runtime/stt.js +480 -0
  120. package/dist/runtime/trace.d.ts +26 -0
  121. package/dist/runtime/trace.js +142 -0
  122. package/dist/runtime/waterfall.d.ts +12 -0
  123. package/dist/runtime/waterfall.js +147 -0
  124. package/dist/schema.d.ts +74 -0
  125. package/dist/schema.js +243 -0
  126. package/dist/serve.d.ts +1 -0
  127. package/dist/serve.js +1 -0
  128. package/dist/server/index.d.ts +3 -0
  129. package/dist/server/index.js +2 -0
  130. package/dist/server/serve.d.ts +64 -0
  131. package/dist/server/serve.js +1110 -0
  132. package/dist/server/types.d.ts +136 -0
  133. package/dist/server/types.js +86 -0
  134. package/dist/stealth/profiles.d.ts +4 -0
  135. package/dist/stealth/profiles.js +259 -0
  136. package/dist/stream.d.ts +44 -0
  137. package/dist/stream.js +151 -0
  138. package/dist/testing/helpers.d.ts +23 -0
  139. package/dist/testing/helpers.js +95 -0
  140. package/dist/testing/index.d.ts +2 -0
  141. package/dist/testing/index.js +2 -0
  142. package/dist/testing/run.d.ts +34 -0
  143. package/dist/testing/run.js +303 -0
  144. package/dist/types.d.ts +1326 -0
  145. package/dist/types.js +61 -0
  146. package/dist/utils/date.d.ts +6 -0
  147. package/dist/utils/date.js +101 -0
  148. package/dist/utils/parse.d.ts +16 -0
  149. package/dist/utils/parse.js +51 -0
  150. package/dist/utils/text.d.ts +4 -0
  151. package/dist/utils/text.js +14 -0
  152. package/dist/utils/transform.d.ts +8 -0
  153. package/dist/utils/transform.js +48 -0
  154. package/package.json +57 -30
  155. package/src/ceremonies/index.ts +8 -2
  156. package/src/choice-token.ts +165 -0
  157. package/src/cli/commands.ts +34 -11
  158. package/src/cli/create.ts +214 -52
  159. package/src/cli/templates/provider/.dockerignore.tpl +22 -0
  160. package/src/cli/templates/provider/.gitignore.tpl +22 -0
  161. package/src/cli/templates/provider/README.md.tpl +120 -1
  162. package/src/cli/templates/provider/dev.ts.tpl +1 -1
  163. package/src/cli/templates/provider/domain/README.md.tpl +3 -0
  164. package/src/cli/templates/provider/index.ts.tpl +5 -48
  165. package/src/cli/templates/provider/mappers/README.md.tpl +3 -0
  166. package/src/cli/templates/provider/meta.ts.tpl +7 -0
  167. package/src/cli/templates/provider/operations/index.ts.tpl +5 -0
  168. package/src/cli/templates/provider/operations/ping.ts.tpl +24 -0
  169. package/src/cli/templates/provider/schemas/ping.ts.tpl +24 -0
  170. package/src/cli/templates/provider/start.ts.tpl +1 -1
  171. package/src/cli/templates/provider/upstream/README.md.tpl +3 -0
  172. package/src/config/loader.ts +1224 -9
  173. package/src/contract-json.ts +75 -0
  174. package/src/contract-serialization.ts +89 -0
  175. package/src/contract-types.ts +52 -0
  176. package/src/contract.ts +215 -0
  177. package/src/define.ts +1688 -48
  178. package/src/errors.ts +27 -0
  179. package/src/i18n/catalog.ts +277 -0
  180. package/src/i18n/index.ts +2 -0
  181. package/src/i18n/keys.ts +64 -0
  182. package/src/index.ts +174 -9
  183. package/src/lint.ts +547 -73
  184. package/src/observability.ts +41 -0
  185. package/src/provider.ts +104 -4
  186. package/src/public-schema-field-lint.ts +237 -0
  187. package/src/runtime/auth-flow.ts +7 -0
  188. package/src/runtime/browser.ts +762 -51
  189. package/src/runtime/cache.ts +528 -0
  190. package/src/runtime/choice.ts +760 -0
  191. package/src/runtime/executor.ts +32 -3
  192. package/src/runtime/http.ts +939 -195
  193. package/src/runtime/insights.ts +11 -11
  194. package/src/runtime/instrumentation.ts +12 -4
  195. package/src/runtime/key-derivation.ts +1 -1
  196. package/src/runtime/keyring.ts +4 -3
  197. package/src/runtime/proxy-errors.ts +132 -0
  198. package/src/runtime/proxy-telemetry.ts +253 -0
  199. package/src/runtime/redis.ts +116 -0
  200. package/src/runtime/request-options.ts +66 -0
  201. package/src/runtime/state.ts +563 -0
  202. package/src/runtime/stealth.ts +1159 -0
  203. package/src/runtime/stt.ts +629 -0
  204. package/src/runtime/trace.ts +1 -1
  205. package/src/schema.ts +363 -1
  206. package/src/server/serve.ts +1157 -75
  207. package/src/server/types.ts +37 -0
  208. package/src/stream.ts +210 -0
  209. package/src/testing/run.ts +31 -5
  210. package/src/types.ts +1107 -59
  211. package/src/runtime/tls.ts +0 -434
  212. package/src/types/playwright-stealth.d.ts +0 -9
@@ -0,0 +1,760 @@
1
+ import {
2
+ createCipheriv,
3
+ createDecipheriv,
4
+ createHash,
5
+ createHmac,
6
+ randomBytes,
7
+ timingSafeEqual,
8
+ } from "node:crypto";
9
+ import {
10
+ assertFreshProviderChoiceIssuedAt,
11
+ ProviderChoiceTokenError,
12
+ type ProviderChoiceTokenPayload,
13
+ } from "../choice-token";
14
+ import { ProviderError } from "../errors";
15
+ import type {
16
+ CredentialContext,
17
+ EnvContext,
18
+ ProviderChoiceBindingOptions,
19
+ ProviderChoiceContext,
20
+ ProviderChoiceIssueOptions,
21
+ ProviderChoiceParseOptions,
22
+ ProviderChoiceStorageOptions,
23
+ ProviderRequestContext,
24
+ ProviderRuntimeState,
25
+ ProviderStateDurationString,
26
+ } from "../types";
27
+
28
+ export const PROVIDER_RUNTIME_CHOICE_TOKEN_MASTER_SECRET_ENV =
29
+ "APIFUSE__PROVIDER_RUNTIME__CHOICE_TOKEN_MASTER_SECRET";
30
+
31
+ const PRIMARY_CHOICE_TOKEN_KID = "v1";
32
+ const MANAGED_CHOICE_TOKEN_VERSION = 1;
33
+
34
+ type ManagedChoiceEnvelope = {
35
+ readonly v: typeof MANAGED_CHOICE_TOKEN_VERSION;
36
+ readonly provider_id: string;
37
+ readonly purpose: string;
38
+ readonly issued_at_ms: number;
39
+ readonly ttl_ms: number;
40
+ readonly binding?: {
41
+ readonly connection_hash?: string;
42
+ readonly credential_hash?: string;
43
+ };
44
+ readonly payload: ProviderChoiceTokenPayload;
45
+ };
46
+
47
+ type ServerChoiceHandlePayload = {
48
+ readonly storage: "server";
49
+ readonly state_id: string;
50
+ readonly payload_digest: string;
51
+ readonly created_at_ms: number;
52
+ };
53
+
54
+ export type CreateProviderChoiceContextOptions = {
55
+ readonly providerId: string;
56
+ readonly env?: EnvContext;
57
+ readonly request?: ProviderRequestContext;
58
+ readonly credential?: CredentialContext;
59
+ readonly state?: ProviderRuntimeState;
60
+ readonly masterSecret?: string;
61
+ readonly kid?: string;
62
+ };
63
+
64
+ export function createProviderChoiceContext(
65
+ options: CreateProviderChoiceContextOptions,
66
+ ): ProviderChoiceContext {
67
+ const kid = options.kid ?? PRIMARY_CHOICE_TOKEN_KID;
68
+ const resolveMasterSecret = () => resolveChoiceMasterSecret(options);
69
+
70
+ function issue<TPayload extends ProviderChoiceTokenPayload>(
71
+ issueOptions: ProviderChoiceIssueOptions<TPayload> & {
72
+ readonly storage?: { readonly mode: "inline" };
73
+ },
74
+ ): string;
75
+ function issue<TPayload extends ProviderChoiceTokenPayload>(
76
+ issueOptions: ProviderChoiceIssueOptions<TPayload> & {
77
+ readonly storage: Extract<
78
+ ProviderChoiceStorageOptions,
79
+ { readonly mode: "server" }
80
+ >;
81
+ },
82
+ ): Promise<string>;
83
+ function issue<TPayload extends ProviderChoiceTokenPayload>(
84
+ issueOptions: ProviderChoiceIssueOptions<TPayload> & {
85
+ readonly storage: Extract<
86
+ ProviderChoiceStorageOptions,
87
+ { readonly mode: "auto" }
88
+ >;
89
+ },
90
+ ): string | Promise<string>;
91
+ function issue<TPayload extends ProviderChoiceTokenPayload>(
92
+ issueOptions: ProviderChoiceIssueOptions<TPayload>,
93
+ ): string | Promise<string> {
94
+ const issuedAtMs = issueOptions.nowMs ?? Date.now();
95
+ const keys = deriveManagedChoiceKeys({
96
+ masterSecret: resolveMasterSecret(),
97
+ providerId: options.providerId,
98
+ purpose: issueOptions.purpose,
99
+ kid,
100
+ });
101
+ const baseEnvelope: Omit<ManagedChoiceEnvelope, "payload"> = {
102
+ v: MANAGED_CHOICE_TOKEN_VERSION,
103
+ provider_id: options.providerId,
104
+ purpose: issueOptions.purpose,
105
+ issued_at_ms: issuedAtMs,
106
+ ttl_ms: issueOptions.ttlMs,
107
+ binding: createChoiceBinding({
108
+ keys,
109
+ options: issueOptions.bind,
110
+ request: options.request,
111
+ credential: options.credential,
112
+ required: true,
113
+ }),
114
+ };
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
+ const envelope: ManagedChoiceEnvelope = {
131
+ ...baseEnvelope,
132
+ payload: issueOptions.payload,
133
+ };
134
+ return encryptManagedChoiceToken({
135
+ prefix: issueOptions.prefix,
136
+ kid,
137
+ envelope,
138
+ keys,
139
+ });
140
+ }
141
+
142
+ function parse(
143
+ parseOptions: ProviderChoiceParseOptions & {
144
+ readonly storage?: { readonly mode: "inline" };
145
+ },
146
+ ): ProviderChoiceTokenPayload;
147
+ function parse(
148
+ parseOptions: ProviderChoiceParseOptions & {
149
+ readonly storage: Extract<
150
+ ProviderChoiceStorageOptions,
151
+ { readonly mode: "server" }
152
+ >;
153
+ },
154
+ ): Promise<ProviderChoiceTokenPayload>;
155
+ function parse(
156
+ parseOptions: ProviderChoiceParseOptions & {
157
+ readonly storage: Extract<
158
+ ProviderChoiceStorageOptions,
159
+ { readonly mode: "auto" }
160
+ >;
161
+ },
162
+ ): ProviderChoiceTokenPayload | Promise<ProviderChoiceTokenPayload>;
163
+ function parse(
164
+ parseOptions: ProviderChoiceParseOptions,
165
+ ): ProviderChoiceTokenPayload | Promise<ProviderChoiceTokenPayload> {
166
+ const [
167
+ actualPrefix,
168
+ tokenKid,
169
+ encodedIv,
170
+ encryptedPayload,
171
+ authTag,
172
+ signature,
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,
218
+ });
219
+ assertChoiceBindingMatches({
220
+ actual: envelope.binding,
221
+ expected: createChoiceBinding({
222
+ keys,
223
+ options: parseOptions.bind,
224
+ request: options.request,
225
+ credential: options.credential,
226
+ required: true,
227
+ }),
228
+ });
229
+ if (isServerChoiceHandlePayload(envelope.payload)) {
230
+ return parseServerStoredChoice({
231
+ handle: envelope.payload,
232
+ storage: parseOptions.storage,
233
+ contextState: options.state,
234
+ });
235
+ }
236
+ return envelope.payload;
237
+ }
238
+
239
+ return { issue, parse };
240
+ }
241
+
242
+ export function createTestProviderChoiceContext(
243
+ options: Omit<CreateProviderChoiceContextOptions, "masterSecret"> & {
244
+ readonly masterSecret?: string;
245
+ },
246
+ ): ProviderChoiceContext {
247
+ return createProviderChoiceContext({
248
+ ...options,
249
+ masterSecret:
250
+ options.masterSecret ??
251
+ "apifuse-test-provider-runtime-choice-token-master-secret",
252
+ });
253
+ }
254
+
255
+ function resolveChoiceMasterSecret(
256
+ options: CreateProviderChoiceContextOptions,
257
+ ): string {
258
+ const configured =
259
+ options.masterSecret ??
260
+ options.env?.get(PROVIDER_RUNTIME_CHOICE_TOKEN_MASTER_SECRET_ENV);
261
+ const trimmed = configured?.trim();
262
+ if (trimmed) return trimmed;
263
+ throw new ProviderError(
264
+ "Provider runtime choice-token master secret is not configured.",
265
+ {
266
+ code: "CHOICE_TOKEN_MASTER_SECRET_NOT_CONFIGURED",
267
+ category: "internal_error",
268
+ retryable: false,
269
+ details: {
270
+ secret: PROVIDER_RUNTIME_CHOICE_TOKEN_MASTER_SECRET_ENV,
271
+ },
272
+ },
273
+ );
274
+ }
275
+
276
+ type ManagedChoiceKeyInput = {
277
+ readonly masterSecret: string;
278
+ readonly providerId: string;
279
+ readonly purpose: string;
280
+ readonly kid: string;
281
+ };
282
+
283
+ type ManagedChoiceKeys = {
284
+ readonly encryption: Buffer;
285
+ readonly signing: Buffer;
286
+ readonly binding: Buffer;
287
+ };
288
+
289
+ function deriveManagedChoiceKeys(
290
+ input: ManagedChoiceKeyInput,
291
+ ): ManagedChoiceKeys {
292
+ return {
293
+ encryption: deriveManagedChoiceKey(input, "encryption"),
294
+ signing: deriveManagedChoiceKey(input, "signing"),
295
+ binding: deriveManagedChoiceKey(input, "binding"),
296
+ };
297
+ }
298
+
299
+ function deriveManagedChoiceKey(
300
+ input: ManagedChoiceKeyInput,
301
+ usage: "encryption" | "signing" | "binding",
302
+ ): Buffer {
303
+ return createHmac("sha256", input.masterSecret)
304
+ .update("apifuse-provider-choice-token")
305
+ .update("\0")
306
+ .update(input.providerId)
307
+ .update("\0")
308
+ .update(input.purpose)
309
+ .update("\0")
310
+ .update(input.kid)
311
+ .update("\0")
312
+ .update(usage)
313
+ .digest();
314
+ }
315
+
316
+ function encryptManagedChoiceToken(options: {
317
+ readonly prefix: string;
318
+ readonly kid: string;
319
+ readonly envelope: ManagedChoiceEnvelope;
320
+ readonly keys: ManagedChoiceKeys;
321
+ }): string {
322
+ const iv = randomBytes(12);
323
+ const cipher = createCipheriv("aes-256-gcm", options.keys.encryption, iv);
324
+ const encryptedPayload = Buffer.concat([
325
+ cipher.update(JSON.stringify(options.envelope), "utf8"),
326
+ cipher.final(),
327
+ ]).toString("base64url");
328
+ const authTag = cipher.getAuthTag().toString("base64url");
329
+ const encodedIv = iv.toString("base64url");
330
+ const signedBody = [
331
+ options.prefix,
332
+ options.kid,
333
+ encodedIv,
334
+ encryptedPayload,
335
+ authTag,
336
+ ].join(".");
337
+ const signature = createHmac("sha256", options.keys.signing)
338
+ .update(signedBody)
339
+ .digest("base64url");
340
+ return `${signedBody}.${signature}`;
341
+ }
342
+
343
+ async function issueServerStoredChoice<
344
+ TPayload extends ProviderChoiceTokenPayload,
345
+ >(options: {
346
+ readonly baseEnvelope: Omit<ManagedChoiceEnvelope, "payload">;
347
+ readonly issueOptions: ProviderChoiceIssueOptions<TPayload>;
348
+ readonly storage: ServerProviderChoiceStorageOptions;
349
+ readonly contextState?: ProviderRuntimeState;
350
+ readonly kid: string;
351
+ readonly keys: ManagedChoiceKeys;
352
+ readonly issuedAtMs: number;
353
+ }): Promise<string> {
354
+ const serializedPayload = serializeChoicePayload(
355
+ options.issueOptions.payload,
356
+ );
357
+ const payloadBytes = Buffer.byteLength(serializedPayload, "utf8");
358
+ if (payloadBytes > options.storage.maxValueBytes) {
359
+ throw new ProviderError(
360
+ "Provider choice payload exceeds state storage policy.",
361
+ {
362
+ code: "CHOICE_STATE_PAYLOAD_TOO_LARGE",
363
+ category: "input_validation",
364
+ retryable: false,
365
+ details: {
366
+ maxValueBytes: options.storage.maxValueBytes,
367
+ payloadBytes,
368
+ },
369
+ },
370
+ );
371
+ }
372
+ const stateId = `choice_${randomBytes(16).toString("base64url")}`;
373
+ const digest = digestChoicePayload(serializedPayload);
374
+ const namespace = resolveChoiceStateNamespace({
375
+ storage: options.storage,
376
+ contextState: options.contextState,
377
+ ttlMs: options.issueOptions.ttlMs,
378
+ });
379
+ await namespace.set(optionsStateKey(stateId), options.issueOptions.payload, {
380
+ ttl: stateTtl(options.storage, options.issueOptions.ttlMs),
381
+ });
382
+ const envelope: ManagedChoiceEnvelope = {
383
+ ...options.baseEnvelope,
384
+ payload: {
385
+ storage: "server",
386
+ state_id: stateId,
387
+ payload_digest: digest,
388
+ created_at_ms: options.issuedAtMs,
389
+ },
390
+ };
391
+ return encryptManagedChoiceToken({
392
+ prefix: options.issueOptions.prefix,
393
+ kid: options.kid,
394
+ envelope,
395
+ keys: options.keys,
396
+ });
397
+ }
398
+
399
+ async function parseServerStoredChoice(options: {
400
+ readonly handle: ServerChoiceHandlePayload;
401
+ readonly storage?: ProviderChoiceStorageOptions;
402
+ readonly contextState?: ProviderRuntimeState;
403
+ }): Promise<ProviderChoiceTokenPayload> {
404
+ const storage = resolveParseStorage(options.storage);
405
+ const namespace = resolveChoiceStateNamespace({
406
+ storage,
407
+ contextState: options.contextState,
408
+ });
409
+ const record = await namespace.get<ProviderChoiceTokenPayload>(
410
+ optionsStateKey(options.handle.state_id),
411
+ );
412
+ if (!record) {
413
+ throw new ProviderChoiceTokenError(
414
+ "invalid_payload",
415
+ "Provider choice token state payload is missing.",
416
+ );
417
+ }
418
+ const serializedPayload = serializeChoicePayload(record.value);
419
+ assertPayloadDigestMatches({
420
+ actual: digestChoicePayload(serializedPayload),
421
+ expected: options.handle.payload_digest,
422
+ });
423
+ return record.value;
424
+ }
425
+
426
+ type ServerProviderChoiceStorageOptions = Extract<
427
+ ProviderChoiceStorageOptions,
428
+ { readonly mode: "server" | "auto" }
429
+ >;
430
+
431
+ function resolveIssueStorage<TPayload extends ProviderChoiceTokenPayload>(
432
+ storage: ProviderChoiceStorageOptions | undefined,
433
+ payload: TPayload,
434
+ ):
435
+ | { readonly mode: "inline" }
436
+ | {
437
+ readonly mode: "server";
438
+ readonly storage: ServerProviderChoiceStorageOptions;
439
+ } {
440
+ if (!storage || storage.mode === "inline") return { mode: "inline" };
441
+ if (storage.mode === "server") return { mode: "server", storage };
442
+ const payloadBytes = Buffer.byteLength(
443
+ serializeChoicePayload(payload),
444
+ "utf8",
445
+ );
446
+ if (payloadBytes <= storage.maxInlineBytes) return { mode: "inline" };
447
+ return { mode: "server", storage };
448
+ }
449
+
450
+ function resolveParseStorage(
451
+ storage: ProviderChoiceStorageOptions | undefined,
452
+ ): ServerProviderChoiceStorageOptions {
453
+ if (!storage || storage.mode === "inline") {
454
+ throw new ProviderChoiceTokenError(
455
+ "invalid_payload",
456
+ "Provider choice token requires server-side choice storage.",
457
+ );
458
+ }
459
+ return storage;
460
+ }
461
+
462
+ function resolveChoiceStateNamespace(options: {
463
+ readonly storage: ServerProviderChoiceStorageOptions;
464
+ readonly contextState?: ProviderRuntimeState;
465
+ readonly ttlMs?: number;
466
+ }) {
467
+ const state = options.storage.state ?? options.contextState;
468
+ if (!state) {
469
+ throw new ProviderError("Provider choice state storage is not available.", {
470
+ code: "CHOICE_STATE_UNAVAILABLE",
471
+ category: "internal_error",
472
+ retryable: false,
473
+ });
474
+ }
475
+ return state.namespace(options.storage.namespace, {
476
+ defaultTtl: stateTtl(options.storage, options.ttlMs),
477
+ maxTtl: stateTtl(options.storage, options.ttlMs),
478
+ maxEntries: options.storage.maxEntries,
479
+ maxValueBytes: options.storage.maxValueBytes,
480
+ });
481
+ }
482
+
483
+ function stateTtl(
484
+ storage: ServerProviderChoiceStorageOptions,
485
+ ttlMs?: number,
486
+ ): ProviderStateDurationString {
487
+ return storage.ttl ?? `${ttlMs ?? 1}ms`;
488
+ }
489
+
490
+ function optionsStateKey(stateId: string): string {
491
+ return stateId;
492
+ }
493
+
494
+ function serializeChoicePayload(payload: ProviderChoiceTokenPayload): string {
495
+ return JSON.stringify(payload);
496
+ }
497
+
498
+ function digestChoicePayload(serializedPayload: string): string {
499
+ return createHash("sha256").update(serializedPayload).digest("base64url");
500
+ }
501
+
502
+ function isServerChoiceHandlePayload(
503
+ value: ProviderChoiceTokenPayload,
504
+ ): value is ServerChoiceHandlePayload {
505
+ return (
506
+ value.storage === "server" &&
507
+ typeof value.state_id === "string" &&
508
+ typeof value.payload_digest === "string" &&
509
+ typeof value.created_at_ms === "number"
510
+ );
511
+ }
512
+
513
+ function assertPayloadDigestMatches(options: {
514
+ readonly actual: string;
515
+ readonly expected: string;
516
+ }): void {
517
+ const actual = Buffer.from(options.actual);
518
+ const expected = Buffer.from(options.expected);
519
+ if (actual.length !== expected.length || !timingSafeEqual(actual, expected)) {
520
+ throw new ProviderChoiceTokenError(
521
+ "invalid_payload",
522
+ "Provider choice token state payload digest is invalid.",
523
+ );
524
+ }
525
+ }
526
+
527
+ function parseManagedChoiceTokenParts(
528
+ token: string,
529
+ ): readonly [
530
+ string | undefined,
531
+ string | undefined,
532
+ string | undefined,
533
+ string | undefined,
534
+ string | undefined,
535
+ string | undefined,
536
+ ] {
537
+ const parts = token.split(".");
538
+ if (parts.length !== 6) {
539
+ throw new ProviderChoiceTokenError(
540
+ "invalid_shape",
541
+ "Provider choice token shape is invalid.",
542
+ );
543
+ }
544
+ return [parts[0], parts[1], parts[2], parts[3], parts[4], parts[5]];
545
+ }
546
+
547
+ function assertManagedChoiceSignature(options: {
548
+ readonly signedBody: string;
549
+ readonly signature: string;
550
+ readonly signingKey: Buffer;
551
+ }): void {
552
+ const expected = createHmac("sha256", options.signingKey)
553
+ .update(options.signedBody)
554
+ .digest("base64url");
555
+ const actualBuffer = Buffer.from(options.signature);
556
+ const expectedBuffer = Buffer.from(expected);
557
+ if (
558
+ actualBuffer.length !== expectedBuffer.length ||
559
+ !timingSafeEqual(actualBuffer, expectedBuffer)
560
+ ) {
561
+ throw new ProviderChoiceTokenError(
562
+ "invalid_signature",
563
+ "Provider choice token signature is invalid.",
564
+ );
565
+ }
566
+ }
567
+
568
+ function decryptManagedChoiceToken(options: {
569
+ readonly encodedIv: string;
570
+ readonly encryptedPayload: string;
571
+ readonly authTag: string;
572
+ readonly encryptionKey: Buffer;
573
+ }): ManagedChoiceEnvelope {
574
+ try {
575
+ const decipher = createDecipheriv(
576
+ "aes-256-gcm",
577
+ options.encryptionKey,
578
+ Buffer.from(options.encodedIv, "base64url"),
579
+ );
580
+ decipher.setAuthTag(Buffer.from(options.authTag, "base64url"));
581
+ const decrypted = Buffer.concat([
582
+ decipher.update(Buffer.from(options.encryptedPayload, "base64url")),
583
+ decipher.final(),
584
+ ]).toString("utf8");
585
+ const parsed: unknown = JSON.parse(decrypted);
586
+ if (!isManagedChoiceEnvelope(parsed)) {
587
+ throw new ProviderChoiceTokenError(
588
+ "invalid_payload",
589
+ "Provider choice token payload is invalid.",
590
+ );
591
+ }
592
+ return parsed;
593
+ } catch (error) {
594
+ if (error instanceof ProviderChoiceTokenError) {
595
+ throw error;
596
+ }
597
+ throw new ProviderChoiceTokenError(
598
+ "invalid_payload",
599
+ "Provider choice token payload is invalid.",
600
+ );
601
+ }
602
+ }
603
+
604
+ function isManagedChoiceEnvelope(
605
+ value: unknown,
606
+ ): value is ManagedChoiceEnvelope {
607
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
608
+ if (!("payload" in value) || !isChoicePayload(value.payload)) return false;
609
+ return (
610
+ "v" in value &&
611
+ value.v === MANAGED_CHOICE_TOKEN_VERSION &&
612
+ "provider_id" in value &&
613
+ typeof value.provider_id === "string" &&
614
+ "purpose" in value &&
615
+ typeof value.purpose === "string" &&
616
+ "issued_at_ms" in value &&
617
+ typeof value.issued_at_ms === "number" &&
618
+ "ttl_ms" in value &&
619
+ typeof value.ttl_ms === "number" &&
620
+ (!("binding" in value) ||
621
+ value.binding === undefined ||
622
+ isChoiceBinding(value.binding))
623
+ );
624
+ }
625
+
626
+ function isChoicePayload(value: unknown): value is ProviderChoiceTokenPayload {
627
+ return Boolean(value && typeof value === "object" && !Array.isArray(value));
628
+ }
629
+
630
+ function isChoiceBinding(
631
+ value: unknown,
632
+ ): value is ManagedChoiceEnvelope["binding"] {
633
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
634
+ return (
635
+ (!("connection_hash" in value) ||
636
+ typeof value.connection_hash === "string") &&
637
+ (!("credential_hash" in value) || typeof value.credential_hash === "string")
638
+ );
639
+ }
640
+
641
+ function assertManagedChoiceEnvelope(
642
+ envelope: ManagedChoiceEnvelope,
643
+ options: {
644
+ readonly providerId: string;
645
+ readonly purpose: string;
646
+ readonly ttlMs?: number;
647
+ readonly nowMs?: number;
648
+ readonly futureToleranceMs?: number;
649
+ },
650
+ ): void {
651
+ if (
652
+ envelope.provider_id !== options.providerId ||
653
+ envelope.purpose !== options.purpose
654
+ ) {
655
+ throw new ProviderChoiceTokenError(
656
+ "invalid_payload",
657
+ "Provider choice token payload is invalid.",
658
+ );
659
+ }
660
+ assertFreshProviderChoiceIssuedAt(envelope.issued_at_ms, {
661
+ // Clamp to the issuer's embedded TTL so a caller-supplied value cannot
662
+ // 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,
667
+ nowMs: options.nowMs,
668
+ futureToleranceMs: options.futureToleranceMs,
669
+ });
670
+ }
671
+
672
+ function createChoiceBinding(options: {
673
+ readonly keys: ManagedChoiceKeys;
674
+ readonly options?: ProviderChoiceBindingOptions;
675
+ readonly request?: ProviderRequestContext;
676
+ readonly credential?: CredentialContext;
677
+ readonly required: boolean;
678
+ }): ManagedChoiceEnvelope["binding"] {
679
+ const connectionHash = options.options?.connection
680
+ ? hashRequiredConnection(options)
681
+ : undefined;
682
+ const credentialHash = options.options?.credentialKeys?.length
683
+ ? hashCredentialKeys(options)
684
+ : undefined;
685
+ if (!connectionHash && !credentialHash) return undefined;
686
+ return {
687
+ ...(connectionHash ? { connection_hash: connectionHash } : {}),
688
+ ...(credentialHash ? { credential_hash: credentialHash } : {}),
689
+ };
690
+ }
691
+
692
+ function hashRequiredConnection(options: {
693
+ readonly keys: ManagedChoiceKeys;
694
+ readonly request?: ProviderRequestContext;
695
+ readonly required: boolean;
696
+ }): string | undefined {
697
+ const connectionId = options.request?.connectionId;
698
+ if (!connectionId) {
699
+ if (!options.required) return undefined;
700
+ throw new ProviderError(
701
+ "Provider choice tokens require connection context.",
702
+ {
703
+ code: "CHOICE_CONTEXT_REQUIRED",
704
+ category: "input_validation",
705
+ retryable: false,
706
+ },
707
+ );
708
+ }
709
+ return createHmac("sha256", options.keys.binding)
710
+ .update("connection")
711
+ .update("\0")
712
+ .update(connectionId)
713
+ .digest("base64url");
714
+ }
715
+
716
+ function hashCredentialKeys(options: {
717
+ readonly keys: ManagedChoiceKeys;
718
+ readonly options?: ProviderChoiceBindingOptions;
719
+ readonly credential?: CredentialContext;
720
+ }): string {
721
+ const credentialKeys = options.options?.credentialKeys ?? [];
722
+ const material = credentialKeys.map((key) => {
723
+ const value = options.credential?.get(key);
724
+ if (typeof value !== "string" || value.length === 0) {
725
+ throw new ProviderError(
726
+ "Provider choice tokens require configured credential binding.",
727
+ {
728
+ code: "CHOICE_CONTEXT_REQUIRED",
729
+ category: "input_validation",
730
+ retryable: false,
731
+ details: { credentialKey: key },
732
+ },
733
+ );
734
+ }
735
+ return [key, value];
736
+ });
737
+ return createHmac("sha256", options.keys.binding)
738
+ .update("credential")
739
+ .update("\0")
740
+ .update(JSON.stringify(material))
741
+ .digest("base64url");
742
+ }
743
+
744
+ function assertChoiceBindingMatches(options: {
745
+ readonly actual: ManagedChoiceEnvelope["binding"];
746
+ readonly expected: ManagedChoiceEnvelope["binding"];
747
+ }): void {
748
+ if (options.actual?.connection_hash !== options.expected?.connection_hash) {
749
+ throw new ProviderChoiceTokenError(
750
+ "invalid_binding",
751
+ "Provider choice token connection binding is invalid.",
752
+ );
753
+ }
754
+ if (options.actual?.credential_hash !== options.expected?.credential_hash) {
755
+ throw new ProviderChoiceTokenError(
756
+ "invalid_binding",
757
+ "Provider choice token credential binding is invalid.",
758
+ );
759
+ }
760
+ }