@apifuse/provider-sdk 2.1.0-beta.6 → 2.1.0-beta.8

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 (128) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/dist/ceremonies/index.d.ts +41 -0
  3. package/dist/ceremonies/index.js +490 -0
  4. package/dist/choice-token.d.ts +24 -0
  5. package/dist/choice-token.js +74 -0
  6. package/dist/cli/commands.d.ts +10 -0
  7. package/dist/cli/commands.js +80 -0
  8. package/dist/cli/create.d.ts +47 -0
  9. package/dist/cli/create.js +762 -0
  10. package/dist/config/loader.d.ts +107 -0
  11. package/dist/config/loader.js +935 -0
  12. package/dist/contract-json.d.ts +9 -0
  13. package/dist/contract-json.js +51 -0
  14. package/dist/contract-serialization.d.ts +4 -0
  15. package/dist/contract-serialization.js +78 -0
  16. package/dist/contract-types.d.ts +49 -0
  17. package/dist/contract-types.js +1 -0
  18. package/dist/contract.d.ts +6 -0
  19. package/dist/contract.js +155 -0
  20. package/dist/define.d.ts +97 -0
  21. package/dist/define.js +1320 -0
  22. package/dist/dev.d.ts +9 -0
  23. package/dist/dev.js +15 -0
  24. package/dist/errors.d.ts +59 -0
  25. package/dist/errors.js +97 -0
  26. package/dist/i18n/catalog.d.ts +29 -0
  27. package/dist/i18n/catalog.js +159 -0
  28. package/dist/i18n/index.d.ts +2 -0
  29. package/dist/i18n/index.js +2 -0
  30. package/dist/i18n/keys.d.ts +10 -0
  31. package/dist/i18n/keys.js +34 -0
  32. package/dist/index.d.ts +41 -0
  33. package/dist/index.js +37 -0
  34. package/dist/lint.d.ts +73 -0
  35. package/dist/lint.js +702 -0
  36. package/dist/observability.d.ts +5 -0
  37. package/dist/observability.js +39 -0
  38. package/dist/provider.d.ts +9 -0
  39. package/dist/provider.js +8 -0
  40. package/dist/public-schema-field-lint.d.ts +2 -0
  41. package/dist/public-schema-field-lint.js +158 -0
  42. package/dist/recipes/gov-api.d.ts +19 -0
  43. package/dist/recipes/gov-api.js +72 -0
  44. package/dist/recipes/rest-api.d.ts +21 -0
  45. package/dist/recipes/rest-api.js +115 -0
  46. package/dist/runtime/auth-flow.d.ts +14 -0
  47. package/dist/runtime/auth-flow.js +44 -0
  48. package/dist/runtime/browser.d.ts +25 -0
  49. package/dist/runtime/browser.js +1034 -0
  50. package/dist/runtime/cache.d.ts +10 -0
  51. package/dist/runtime/cache.js +372 -0
  52. package/dist/runtime/choice.d.ts +15 -0
  53. package/dist/runtime/choice.js +435 -0
  54. package/dist/runtime/credential.d.ts +8 -0
  55. package/dist/runtime/credential.js +61 -0
  56. package/dist/runtime/env.d.ts +2 -0
  57. package/dist/runtime/env.js +10 -0
  58. package/dist/runtime/executor.d.ts +16 -0
  59. package/dist/runtime/executor.js +51 -0
  60. package/dist/runtime/http.d.ts +8 -0
  61. package/dist/runtime/http.js +706 -0
  62. package/dist/runtime/insights.d.ts +9 -0
  63. package/dist/runtime/insights.js +324 -0
  64. package/dist/runtime/instrumentation.d.ts +8 -0
  65. package/dist/runtime/instrumentation.js +269 -0
  66. package/dist/runtime/key-derivation.d.ts +24 -0
  67. package/dist/runtime/key-derivation.js +73 -0
  68. package/dist/runtime/keyring.d.ts +25 -0
  69. package/dist/runtime/keyring.js +93 -0
  70. package/dist/runtime/namespace.d.ts +9 -0
  71. package/dist/runtime/namespace.js +19 -0
  72. package/dist/runtime/otlp.d.ts +39 -0
  73. package/dist/runtime/otlp.js +103 -0
  74. package/dist/runtime/perf.d.ts +12 -0
  75. package/dist/runtime/perf.js +52 -0
  76. package/dist/runtime/prevalidate.d.ts +12 -0
  77. package/dist/runtime/prevalidate.js +173 -0
  78. package/dist/runtime/provider.d.ts +2 -0
  79. package/dist/runtime/provider.js +11 -0
  80. package/dist/runtime/proxy-errors.d.ts +21 -0
  81. package/dist/runtime/proxy-errors.js +83 -0
  82. package/dist/runtime/proxy-telemetry.d.ts +8 -0
  83. package/dist/runtime/proxy-telemetry.js +174 -0
  84. package/dist/runtime/redis.d.ts +17 -0
  85. package/dist/runtime/redis.js +82 -0
  86. package/dist/runtime/request-options.d.ts +3 -0
  87. package/dist/runtime/request-options.js +42 -0
  88. package/dist/runtime/state.d.ts +17 -0
  89. package/dist/runtime/state.js +344 -0
  90. package/dist/runtime/stealth.d.ts +18 -0
  91. package/dist/runtime/stealth.js +827 -0
  92. package/dist/runtime/stt.d.ts +22 -0
  93. package/dist/runtime/stt.js +480 -0
  94. package/dist/runtime/trace.d.ts +26 -0
  95. package/dist/runtime/trace.js +142 -0
  96. package/dist/runtime/waterfall.d.ts +12 -0
  97. package/dist/runtime/waterfall.js +147 -0
  98. package/dist/schema.d.ts +74 -0
  99. package/dist/schema.js +243 -0
  100. package/dist/serve.d.ts +1 -0
  101. package/dist/serve.js +1 -0
  102. package/dist/server/index.d.ts +3 -0
  103. package/dist/server/index.js +2 -0
  104. package/dist/server/serve.d.ts +64 -0
  105. package/dist/server/serve.js +1110 -0
  106. package/dist/server/types.d.ts +136 -0
  107. package/dist/server/types.js +86 -0
  108. package/dist/stealth/profiles.d.ts +4 -0
  109. package/dist/stealth/profiles.js +259 -0
  110. package/dist/stream.d.ts +44 -0
  111. package/dist/stream.js +151 -0
  112. package/dist/testing/helpers.d.ts +23 -0
  113. package/dist/testing/helpers.js +95 -0
  114. package/dist/testing/index.d.ts +2 -0
  115. package/dist/testing/index.js +2 -0
  116. package/dist/testing/run.d.ts +34 -0
  117. package/dist/testing/run.js +303 -0
  118. package/dist/types.d.ts +1324 -0
  119. package/dist/types.js +61 -0
  120. package/dist/utils/date.d.ts +6 -0
  121. package/dist/utils/date.js +101 -0
  122. package/dist/utils/parse.d.ts +16 -0
  123. package/dist/utils/parse.js +51 -0
  124. package/dist/utils/text.d.ts +4 -0
  125. package/dist/utils/text.js +14 -0
  126. package/dist/utils/transform.d.ts +8 -0
  127. package/dist/utils/transform.js +48 -0
  128. package/package.json +109 -107
@@ -0,0 +1,435 @@
1
+ import { createCipheriv, createDecipheriv, createHash, createHmac, randomBytes, timingSafeEqual, } from "node:crypto";
2
+ import { assertFreshProviderChoiceIssuedAt, ProviderChoiceTokenError, } from "../choice-token";
3
+ import { ProviderError } from "../errors";
4
+ export const PROVIDER_RUNTIME_CHOICE_TOKEN_MASTER_SECRET_ENV = "APIFUSE__PROVIDER_RUNTIME__CHOICE_TOKEN_MASTER_SECRET";
5
+ const PRIMARY_CHOICE_TOKEN_KID = "v1";
6
+ const MANAGED_CHOICE_TOKEN_VERSION = 1;
7
+ export function createProviderChoiceContext(options) {
8
+ const kid = options.kid ?? PRIMARY_CHOICE_TOKEN_KID;
9
+ const resolveMasterSecret = () => resolveChoiceMasterSecret(options);
10
+ function issue(issueOptions) {
11
+ const issuedAtMs = issueOptions.nowMs ?? Date.now();
12
+ const keys = deriveManagedChoiceKeys({
13
+ masterSecret: resolveMasterSecret(),
14
+ providerId: options.providerId,
15
+ purpose: issueOptions.purpose,
16
+ kid,
17
+ });
18
+ const baseEnvelope = {
19
+ v: MANAGED_CHOICE_TOKEN_VERSION,
20
+ provider_id: options.providerId,
21
+ purpose: issueOptions.purpose,
22
+ issued_at_ms: issuedAtMs,
23
+ ttl_ms: issueOptions.ttlMs,
24
+ binding: createChoiceBinding({
25
+ keys,
26
+ options: issueOptions.bind,
27
+ request: options.request,
28
+ credential: options.credential,
29
+ required: true,
30
+ }),
31
+ };
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
+ const envelope = {
45
+ ...baseEnvelope,
46
+ payload: issueOptions.payload,
47
+ };
48
+ return encryptManagedChoiceToken({
49
+ prefix: issueOptions.prefix,
50
+ kid,
51
+ envelope,
52
+ keys,
53
+ });
54
+ }
55
+ function parse(parseOptions) {
56
+ const [actualPrefix, tokenKid, encodedIv, encryptedPayload, authTag, signature,] = parseManagedChoiceTokenParts(parseOptions.token);
57
+ if (actualPrefix !== parseOptions.prefix ||
58
+ tokenKid !== kid ||
59
+ !encodedIv ||
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 = [
72
+ parseOptions.prefix,
73
+ tokenKid,
74
+ encodedIv,
75
+ encryptedPayload,
76
+ authTag,
77
+ ].join(".");
78
+ assertManagedChoiceSignature({
79
+ signedBody,
80
+ signature,
81
+ signingKey: keys.signing,
82
+ });
83
+ const envelope = decryptManagedChoiceToken({
84
+ encodedIv,
85
+ encryptedPayload,
86
+ authTag,
87
+ encryptionKey: keys.encryption,
88
+ });
89
+ assertManagedChoiceEnvelope(envelope, {
90
+ providerId: options.providerId,
91
+ purpose: parseOptions.purpose,
92
+ ttlMs: parseOptions.ttlMs,
93
+ nowMs: parseOptions.nowMs,
94
+ futureToleranceMs: parseOptions.futureToleranceMs,
95
+ });
96
+ assertChoiceBindingMatches({
97
+ actual: envelope.binding,
98
+ expected: createChoiceBinding({
99
+ keys,
100
+ options: parseOptions.bind,
101
+ request: options.request,
102
+ credential: options.credential,
103
+ required: true,
104
+ }),
105
+ });
106
+ if (isServerChoiceHandlePayload(envelope.payload)) {
107
+ return parseServerStoredChoice({
108
+ handle: envelope.payload,
109
+ storage: parseOptions.storage,
110
+ contextState: options.state,
111
+ });
112
+ }
113
+ return envelope.payload;
114
+ }
115
+ return { issue, parse };
116
+ }
117
+ export function createTestProviderChoiceContext(options) {
118
+ return createProviderChoiceContext({
119
+ ...options,
120
+ masterSecret: options.masterSecret ??
121
+ "apifuse-test-provider-runtime-choice-token-master-secret",
122
+ });
123
+ }
124
+ function resolveChoiceMasterSecret(options) {
125
+ const configured = options.masterSecret ??
126
+ options.env?.get(PROVIDER_RUNTIME_CHOICE_TOKEN_MASTER_SECRET_ENV);
127
+ const trimmed = configured?.trim();
128
+ if (trimmed)
129
+ return trimmed;
130
+ throw new ProviderError("Provider runtime choice-token master secret is not configured.", {
131
+ code: "CHOICE_TOKEN_MASTER_SECRET_NOT_CONFIGURED",
132
+ category: "internal_error",
133
+ retryable: false,
134
+ details: {
135
+ secret: PROVIDER_RUNTIME_CHOICE_TOKEN_MASTER_SECRET_ENV,
136
+ },
137
+ });
138
+ }
139
+ function deriveManagedChoiceKeys(input) {
140
+ return {
141
+ encryption: deriveManagedChoiceKey(input, "encryption"),
142
+ signing: deriveManagedChoiceKey(input, "signing"),
143
+ binding: deriveManagedChoiceKey(input, "binding"),
144
+ };
145
+ }
146
+ function deriveManagedChoiceKey(input, usage) {
147
+ return createHmac("sha256", input.masterSecret)
148
+ .update("apifuse-provider-choice-token")
149
+ .update("\0")
150
+ .update(input.providerId)
151
+ .update("\0")
152
+ .update(input.purpose)
153
+ .update("\0")
154
+ .update(input.kid)
155
+ .update("\0")
156
+ .update(usage)
157
+ .digest();
158
+ }
159
+ function encryptManagedChoiceToken(options) {
160
+ const iv = randomBytes(12);
161
+ const cipher = createCipheriv("aes-256-gcm", options.keys.encryption, iv);
162
+ const encryptedPayload = Buffer.concat([
163
+ cipher.update(JSON.stringify(options.envelope), "utf8"),
164
+ cipher.final(),
165
+ ]).toString("base64url");
166
+ const authTag = cipher.getAuthTag().toString("base64url");
167
+ const encodedIv = iv.toString("base64url");
168
+ const signedBody = [
169
+ options.prefix,
170
+ options.kid,
171
+ encodedIv,
172
+ encryptedPayload,
173
+ authTag,
174
+ ].join(".");
175
+ const signature = createHmac("sha256", options.keys.signing)
176
+ .update(signedBody)
177
+ .digest("base64url");
178
+ return `${signedBody}.${signature}`;
179
+ }
180
+ async function issueServerStoredChoice(options) {
181
+ const serializedPayload = serializeChoicePayload(options.issueOptions.payload);
182
+ const payloadBytes = Buffer.byteLength(serializedPayload, "utf8");
183
+ if (payloadBytes > options.storage.maxValueBytes) {
184
+ throw new ProviderError("Provider choice payload exceeds state storage policy.", {
185
+ code: "CHOICE_STATE_PAYLOAD_TOO_LARGE",
186
+ category: "input_validation",
187
+ retryable: false,
188
+ details: {
189
+ maxValueBytes: options.storage.maxValueBytes,
190
+ payloadBytes,
191
+ },
192
+ });
193
+ }
194
+ const stateId = `choice_${randomBytes(16).toString("base64url")}`;
195
+ const digest = digestChoicePayload(serializedPayload);
196
+ const namespace = resolveChoiceStateNamespace({
197
+ storage: options.storage,
198
+ contextState: options.contextState,
199
+ ttlMs: options.issueOptions.ttlMs,
200
+ });
201
+ await namespace.set(optionsStateKey(stateId), options.issueOptions.payload, {
202
+ ttl: stateTtl(options.storage, options.issueOptions.ttlMs),
203
+ });
204
+ const envelope = {
205
+ ...options.baseEnvelope,
206
+ payload: {
207
+ storage: "server",
208
+ state_id: stateId,
209
+ payload_digest: digest,
210
+ created_at_ms: options.issuedAtMs,
211
+ },
212
+ };
213
+ return encryptManagedChoiceToken({
214
+ prefix: options.issueOptions.prefix,
215
+ kid: options.kid,
216
+ envelope,
217
+ keys: options.keys,
218
+ });
219
+ }
220
+ async function parseServerStoredChoice(options) {
221
+ const storage = resolveParseStorage(options.storage);
222
+ const namespace = resolveChoiceStateNamespace({
223
+ storage,
224
+ contextState: options.contextState,
225
+ });
226
+ const record = await namespace.get(optionsStateKey(options.handle.state_id));
227
+ if (!record) {
228
+ throw new ProviderChoiceTokenError("invalid_payload", "Provider choice token state payload is missing.");
229
+ }
230
+ const serializedPayload = serializeChoicePayload(record.value);
231
+ assertPayloadDigestMatches({
232
+ actual: digestChoicePayload(serializedPayload),
233
+ expected: options.handle.payload_digest,
234
+ });
235
+ return record.value;
236
+ }
237
+ function resolveIssueStorage(storage, payload) {
238
+ if (!storage || storage.mode === "inline")
239
+ return { mode: "inline" };
240
+ if (storage.mode === "server")
241
+ return { mode: "server", storage };
242
+ const payloadBytes = Buffer.byteLength(serializeChoicePayload(payload), "utf8");
243
+ if (payloadBytes <= storage.maxInlineBytes)
244
+ return { mode: "inline" };
245
+ return { mode: "server", storage };
246
+ }
247
+ function resolveParseStorage(storage) {
248
+ if (!storage || storage.mode === "inline") {
249
+ throw new ProviderChoiceTokenError("invalid_payload", "Provider choice token requires server-side choice storage.");
250
+ }
251
+ return storage;
252
+ }
253
+ function resolveChoiceStateNamespace(options) {
254
+ const state = options.storage.state ?? options.contextState;
255
+ if (!state) {
256
+ throw new ProviderError("Provider choice state storage is not available.", {
257
+ code: "CHOICE_STATE_UNAVAILABLE",
258
+ category: "internal_error",
259
+ retryable: false,
260
+ });
261
+ }
262
+ return state.namespace(options.storage.namespace, {
263
+ defaultTtl: stateTtl(options.storage, options.ttlMs),
264
+ maxTtl: stateTtl(options.storage, options.ttlMs),
265
+ maxEntries: options.storage.maxEntries,
266
+ maxValueBytes: options.storage.maxValueBytes,
267
+ });
268
+ }
269
+ function stateTtl(storage, ttlMs) {
270
+ return storage.ttl ?? `${ttlMs ?? 1}ms`;
271
+ }
272
+ function optionsStateKey(stateId) {
273
+ return stateId;
274
+ }
275
+ function serializeChoicePayload(payload) {
276
+ return JSON.stringify(payload);
277
+ }
278
+ function digestChoicePayload(serializedPayload) {
279
+ return createHash("sha256").update(serializedPayload).digest("base64url");
280
+ }
281
+ function isServerChoiceHandlePayload(value) {
282
+ return (value.storage === "server" &&
283
+ typeof value.state_id === "string" &&
284
+ typeof value.payload_digest === "string" &&
285
+ typeof value.created_at_ms === "number");
286
+ }
287
+ function assertPayloadDigestMatches(options) {
288
+ const actual = Buffer.from(options.actual);
289
+ const expected = Buffer.from(options.expected);
290
+ if (actual.length !== expected.length || !timingSafeEqual(actual, expected)) {
291
+ throw new ProviderChoiceTokenError("invalid_payload", "Provider choice token state payload digest is invalid.");
292
+ }
293
+ }
294
+ function parseManagedChoiceTokenParts(token) {
295
+ const parts = token.split(".");
296
+ if (parts.length !== 6) {
297
+ throw new ProviderChoiceTokenError("invalid_shape", "Provider choice token shape is invalid.");
298
+ }
299
+ return [parts[0], parts[1], parts[2], parts[3], parts[4], parts[5]];
300
+ }
301
+ function assertManagedChoiceSignature(options) {
302
+ const expected = createHmac("sha256", options.signingKey)
303
+ .update(options.signedBody)
304
+ .digest("base64url");
305
+ const actualBuffer = Buffer.from(options.signature);
306
+ const expectedBuffer = Buffer.from(expected);
307
+ if (actualBuffer.length !== expectedBuffer.length ||
308
+ !timingSafeEqual(actualBuffer, expectedBuffer)) {
309
+ throw new ProviderChoiceTokenError("invalid_signature", "Provider choice token signature is invalid.");
310
+ }
311
+ }
312
+ function decryptManagedChoiceToken(options) {
313
+ try {
314
+ const decipher = createDecipheriv("aes-256-gcm", options.encryptionKey, Buffer.from(options.encodedIv, "base64url"));
315
+ decipher.setAuthTag(Buffer.from(options.authTag, "base64url"));
316
+ const decrypted = Buffer.concat([
317
+ decipher.update(Buffer.from(options.encryptedPayload, "base64url")),
318
+ decipher.final(),
319
+ ]).toString("utf8");
320
+ const parsed = JSON.parse(decrypted);
321
+ if (!isManagedChoiceEnvelope(parsed)) {
322
+ throw new ProviderChoiceTokenError("invalid_payload", "Provider choice token payload is invalid.");
323
+ }
324
+ return parsed;
325
+ }
326
+ catch (error) {
327
+ if (error instanceof ProviderChoiceTokenError) {
328
+ throw error;
329
+ }
330
+ throw new ProviderChoiceTokenError("invalid_payload", "Provider choice token payload is invalid.");
331
+ }
332
+ }
333
+ function isManagedChoiceEnvelope(value) {
334
+ if (!value || typeof value !== "object" || Array.isArray(value))
335
+ return false;
336
+ if (!("payload" in value) || !isChoicePayload(value.payload))
337
+ return false;
338
+ return ("v" in value &&
339
+ value.v === MANAGED_CHOICE_TOKEN_VERSION &&
340
+ "provider_id" in value &&
341
+ typeof value.provider_id === "string" &&
342
+ "purpose" in value &&
343
+ typeof value.purpose === "string" &&
344
+ "issued_at_ms" in value &&
345
+ typeof value.issued_at_ms === "number" &&
346
+ "ttl_ms" in value &&
347
+ typeof value.ttl_ms === "number" &&
348
+ (!("binding" in value) ||
349
+ value.binding === undefined ||
350
+ isChoiceBinding(value.binding)));
351
+ }
352
+ function isChoicePayload(value) {
353
+ return Boolean(value && typeof value === "object" && !Array.isArray(value));
354
+ }
355
+ function isChoiceBinding(value) {
356
+ if (!value || typeof value !== "object" || Array.isArray(value))
357
+ return false;
358
+ return ((!("connection_hash" in value) ||
359
+ typeof value.connection_hash === "string") &&
360
+ (!("credential_hash" in value) || typeof value.credential_hash === "string"));
361
+ }
362
+ function assertManagedChoiceEnvelope(envelope, options) {
363
+ if (envelope.provider_id !== options.providerId ||
364
+ envelope.purpose !== options.purpose) {
365
+ throw new ProviderChoiceTokenError("invalid_payload", "Provider choice token payload is invalid.");
366
+ }
367
+ assertFreshProviderChoiceIssuedAt(envelope.issued_at_ms, {
368
+ // Clamp to the issuer's embedded TTL so a caller-supplied value cannot
369
+ // silently extend token validity past the deadline the issuer intended.
370
+ ttlMs: options.ttlMs != null
371
+ ? Math.min(options.ttlMs, envelope.ttl_ms)
372
+ : envelope.ttl_ms,
373
+ nowMs: options.nowMs,
374
+ futureToleranceMs: options.futureToleranceMs,
375
+ });
376
+ }
377
+ function createChoiceBinding(options) {
378
+ const connectionHash = options.options?.connection
379
+ ? hashRequiredConnection(options)
380
+ : undefined;
381
+ const credentialHash = options.options?.credentialKeys?.length
382
+ ? hashCredentialKeys(options)
383
+ : undefined;
384
+ if (!connectionHash && !credentialHash)
385
+ return undefined;
386
+ return {
387
+ ...(connectionHash ? { connection_hash: connectionHash } : {}),
388
+ ...(credentialHash ? { credential_hash: credentialHash } : {}),
389
+ };
390
+ }
391
+ function hashRequiredConnection(options) {
392
+ const connectionId = options.request?.connectionId;
393
+ if (!connectionId) {
394
+ if (!options.required)
395
+ return undefined;
396
+ throw new ProviderError("Provider choice tokens require connection context.", {
397
+ code: "CHOICE_CONTEXT_REQUIRED",
398
+ category: "input_validation",
399
+ retryable: false,
400
+ });
401
+ }
402
+ return createHmac("sha256", options.keys.binding)
403
+ .update("connection")
404
+ .update("\0")
405
+ .update(connectionId)
406
+ .digest("base64url");
407
+ }
408
+ function hashCredentialKeys(options) {
409
+ const credentialKeys = options.options?.credentialKeys ?? [];
410
+ const material = credentialKeys.map((key) => {
411
+ const value = options.credential?.get(key);
412
+ if (typeof value !== "string" || value.length === 0) {
413
+ throw new ProviderError("Provider choice tokens require configured credential binding.", {
414
+ code: "CHOICE_CONTEXT_REQUIRED",
415
+ category: "input_validation",
416
+ retryable: false,
417
+ details: { credentialKey: key },
418
+ });
419
+ }
420
+ return [key, value];
421
+ });
422
+ return createHmac("sha256", options.keys.binding)
423
+ .update("credential")
424
+ .update("\0")
425
+ .update(JSON.stringify(material))
426
+ .digest("base64url");
427
+ }
428
+ function assertChoiceBindingMatches(options) {
429
+ if (options.actual?.connection_hash !== options.expected?.connection_hash) {
430
+ throw new ProviderChoiceTokenError("invalid_binding", "Provider choice token connection binding is invalid.");
431
+ }
432
+ if (options.actual?.credential_hash !== options.expected?.credential_hash) {
433
+ throw new ProviderChoiceTokenError("invalid_binding", "Provider choice token credential binding is invalid.");
434
+ }
435
+ }
@@ -0,0 +1,8 @@
1
+ import type { AuthMode, CredentialContext } from "../types";
2
+ export interface CreateCredentialContextOptions {
3
+ allowedKeys?: string[];
4
+ mode?: AuthMode;
5
+ scopes?: string[];
6
+ values?: Record<string, string>;
7
+ }
8
+ export declare function createCredentialContext(options?: CreateCredentialContextOptions): CredentialContext;
@@ -0,0 +1,61 @@
1
+ import { CredentialModeError } from "../errors";
2
+ function getAllowedKeys(allowedKeys, values) {
3
+ if (allowedKeys) {
4
+ return allowedKeys;
5
+ }
6
+ return Object.keys(values);
7
+ }
8
+ function normalizeScopes(mode, values, scopes) {
9
+ if (mode !== "oauth2") {
10
+ return [];
11
+ }
12
+ if (scopes) {
13
+ return [...scopes];
14
+ }
15
+ const rawScopes = values.scope ?? values.scopes;
16
+ if (!rawScopes) {
17
+ return [];
18
+ }
19
+ return rawScopes
20
+ .split(/[\s,]+/)
21
+ .map((scope) => scope.trim())
22
+ .filter((scope) => scope.length > 0);
23
+ }
24
+ export function createCredentialContext(options = {}) {
25
+ const mode = options.mode ?? "none";
26
+ const values = options.values ?? {};
27
+ const allowedKeys = getAllowedKeys(options.allowedKeys, values);
28
+ const allowedKeySet = new Set(allowedKeys);
29
+ const normalizedScopes = normalizeScopes(mode, values, options.scopes);
30
+ return {
31
+ mode,
32
+ get(key) {
33
+ if (!allowedKeySet.has(key)) {
34
+ return undefined;
35
+ }
36
+ return values[key];
37
+ },
38
+ getAll() {
39
+ const result = {};
40
+ for (const key of allowedKeys) {
41
+ const value = values[key];
42
+ if (value !== undefined) {
43
+ result[key] = value;
44
+ }
45
+ }
46
+ return result;
47
+ },
48
+ getAccessToken() {
49
+ if (mode !== "oauth2") {
50
+ throw new CredentialModeError("Access tokens are only available for oauth2 credential mode.");
51
+ }
52
+ return values.access_token;
53
+ },
54
+ getScopes() {
55
+ if (mode !== "oauth2") {
56
+ throw new CredentialModeError("OAuth scopes are only available for oauth2 credential mode.");
57
+ }
58
+ return [...normalizedScopes];
59
+ },
60
+ };
61
+ }
@@ -0,0 +1,2 @@
1
+ import type { EnvContext } from "../types";
2
+ export declare function createEnvContext(allowedKeys?: string[]): EnvContext;
@@ -0,0 +1,10 @@
1
+ export function createEnvContext(allowedKeys) {
2
+ return {
3
+ get(key) {
4
+ if (allowedKeys && !allowedKeys.includes(key)) {
5
+ return undefined;
6
+ }
7
+ return process.env[key];
8
+ },
9
+ };
10
+ }
@@ -0,0 +1,16 @@
1
+ import type { ProviderContext, ProviderDefinition } from "../types";
2
+ export declare function isStreamingOperation(provider: ProviderDefinition, operationId: string): boolean;
3
+ /**
4
+ * Execute a provider operation by calling its handler.
5
+ *
6
+ * SDK auto-wraps every handler call with:
7
+ * 1. Input Zod validation
8
+ * 2. Auth auto-refresh (if auth configured)
9
+ * 3. Trace span
10
+ * 4. Output Zod validation
11
+ *
12
+ * @see openspec/provider-sdk/03-sdk-core.md §3.6
13
+ */
14
+ export declare function executeOperation(provider: ProviderDefinition, operationId: string, ctx: ProviderContext, input: unknown, _options?: {
15
+ skipAuth?: boolean;
16
+ }): Promise<unknown>;
@@ -0,0 +1,51 @@
1
+ import { ProviderError, SessionExpiredError } from "../errors";
2
+ import { parseSchema } from "../schema";
3
+ export function isStreamingOperation(provider, operationId) {
4
+ const kind = provider.operations[operationId]?.transport?.kind ?? "json";
5
+ return kind !== "json";
6
+ }
7
+ /**
8
+ * Execute a provider operation by calling its handler.
9
+ *
10
+ * SDK auto-wraps every handler call with:
11
+ * 1. Input Zod validation
12
+ * 2. Auth auto-refresh (if auth configured)
13
+ * 3. Trace span
14
+ * 4. Output Zod validation
15
+ *
16
+ * @see openspec/provider-sdk/03-sdk-core.md §3.6
17
+ */
18
+ export async function executeOperation(provider, operationId, ctx, input, _options) {
19
+ const operation = provider.operations[operationId];
20
+ if (!operation) {
21
+ throw new ProviderError(`Unknown operation: ${provider.id}/${operationId}`, {
22
+ code: "NOT_FOUND",
23
+ fix: `Valid operations: ${Object.keys(provider.operations).join(", ")}`,
24
+ });
25
+ }
26
+ const validatedInput = await parseSchema(operation.input, input, `operations.${operationId}.input`);
27
+ const execute = () => ctx.trace.span(`handler:${operationId}`, () => Promise.resolve(operation.handler(ctx, validatedInput)));
28
+ let result;
29
+ try {
30
+ result = await execute();
31
+ }
32
+ catch (error) {
33
+ // Session expiry is renewed by Credential Service via the /auth/refresh
34
+ // route, NOT in-process here: this executor cannot mutate ctx.credential,
35
+ // so an in-process retry would just repeat the call with the same stale
36
+ // credential (and risk repeating partial side-effects). Instead we surface
37
+ // the expiry so Credential Service refreshes and re-drives the operation
38
+ // with a fresh credential. `retryOnAuthRefresh` declares that this
39
+ // operation is safe to re-drive after refresh, which we signal by marking
40
+ // the surfaced error retryable; non-idempotent operations (the default)
41
+ // stay non-retryable so they are not auto-re-driven. See design.md §4.3 D3.
42
+ if (error instanceof SessionExpiredError && operation.retryOnAuthRefresh) {
43
+ throw new SessionExpiredError(error.message, { retryable: true });
44
+ }
45
+ throw error;
46
+ }
47
+ if (isStreamingOperation(provider, operationId)) {
48
+ return result;
49
+ }
50
+ return parseSchema(operation.output, result, `operations.${operationId}.output`);
51
+ }
@@ -0,0 +1,8 @@
1
+ import type { ProxyResolutionOptions } from "../config/loader";
2
+ import type { HttpClient, HttpRetrySummary } from "../types";
3
+ export type HttpClientOptions = ProxyResolutionOptions & {
4
+ warn?: (message: string) => void;
5
+ userAgent?: string;
6
+ onRetrySummary?: (summary: HttpRetrySummary) => void;
7
+ };
8
+ export declare function createHttpClient(baseUrl?: string, clientOptions?: HttpClientOptions): HttpClient;