@402flow/sdk 0.1.0-alpha.2 → 0.1.0-alpha.21

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/dist/index.js CHANGED
@@ -1,6 +1,54 @@
1
+ /**
2
+ * Core public client surface for @402flow/sdk.
3
+ *
4
+ * The SDK exposes two primary integration styles:
5
+ * 1. fetchPaid() for the fastest end-to-end paid request path
6
+ * 2. preparePaidRequest() plus executePreparedRequest() when the caller wants
7
+ * an explicit check/revise/execute loop
8
+ */
1
9
  import { createHash } from 'node:crypto';
2
10
  import { detectChallengeFromResponse, } from './challenge-detection.js';
3
- import { sdkPaymentDecisionRequestSchema, sdkPaymentDecisionResponseSchema, sdkReceiptResponseSchema, } from './contracts.js';
11
+ import { monetaryAmountToMinorUnits, paidRequestChallengeSchema, paidRequestHttpRequestSchema, sdkPaymentDecisionRequestSchema, sdkPaymentDecisionResponseSchema, sdkReceiptResponseSchema, } from './contracts.js';
12
+ import { sdkClientVersion, sdkClientVersionHeaderName, } from './version.js';
13
+ /**
14
+ * Thrown for all non-success paid outcomes. The original typed failure payload is
15
+ * preserved on details so callers can branch on kind without reparsing responses.
16
+ */
17
+ export class FetchPaidError extends Error {
18
+ details;
19
+ kind;
20
+ protocol;
21
+ response;
22
+ reason;
23
+ decision;
24
+ paidRequestId;
25
+ paymentAttemptId;
26
+ receiptId;
27
+ receipt;
28
+ policyReviewEventId;
29
+ constructor(details) {
30
+ super(`${details.kind}: ${details.reason}`);
31
+ this.name = 'FetchPaidError';
32
+ this.details = details;
33
+ this.kind = details.kind;
34
+ this.protocol = details.protocol;
35
+ this.response = details.response;
36
+ this.reason = details.reason;
37
+ this.decision = details.decision;
38
+ this.paidRequestId = 'paidRequestId' in details ? details.paidRequestId : undefined;
39
+ this.paymentAttemptId =
40
+ 'paymentAttemptId' in details ? details.paymentAttemptId : undefined;
41
+ this.receiptId = 'receiptId' in details ? details.receiptId : undefined;
42
+ this.receipt = 'receipt' in details ? details.receipt : undefined;
43
+ this.policyReviewEventId =
44
+ 'policyReviewEventId' in details ? details.policyReviewEventId : undefined;
45
+ Object.setPrototypeOf(this, new.target.prototype);
46
+ }
47
+ }
48
+ /** Type guard for callers that catch unknown errors around fetchPaid flows. */
49
+ export function isFetchPaidError(error) {
50
+ return error instanceof FetchPaidError;
51
+ }
4
52
  const defaultRuntimeTokenRefreshWindowMs = 30_000;
5
53
  function trimTrailingSlash(value) {
6
54
  return value.endsWith('/') ? value.slice(0, -1) : value;
@@ -30,7 +78,93 @@ function createMerchantResponse(merchantResponse) {
30
78
  headers: merchantResponse.headers,
31
79
  });
32
80
  }
33
- function getReplayableRequestBody(body) {
81
+ function createRawResponse(status, body, headers) {
82
+ return new Response(body, {
83
+ status,
84
+ ...(headers ? { headers } : {}),
85
+ });
86
+ }
87
+ function isRecord(value) {
88
+ return typeof value === 'object' && value !== null;
89
+ }
90
+ function getControlPlaneErrorMessage(body, fallback) {
91
+ if (!isRecord(body)) {
92
+ return fallback;
93
+ }
94
+ const message = typeof body.message === 'string' && body.message.length > 0
95
+ ? body.message
96
+ : fallback;
97
+ const issues = body.issues;
98
+ if (!isRecord(issues)) {
99
+ return message;
100
+ }
101
+ const details = [];
102
+ const formErrors = issues.formErrors;
103
+ if (Array.isArray(formErrors)) {
104
+ for (const entry of formErrors) {
105
+ if (typeof entry === 'string' && entry.length > 0) {
106
+ details.push(entry);
107
+ }
108
+ }
109
+ }
110
+ const fieldErrors = issues.fieldErrors;
111
+ if (isRecord(fieldErrors)) {
112
+ for (const [field, value] of Object.entries(fieldErrors)) {
113
+ if (!Array.isArray(value)) {
114
+ continue;
115
+ }
116
+ const fieldMessages = value.filter((entry) => typeof entry === 'string' && entry.length > 0);
117
+ if (fieldMessages.length > 0) {
118
+ details.push(`${field}: ${fieldMessages.join(', ')}`);
119
+ }
120
+ }
121
+ }
122
+ return details.length > 0 ? `${message} ${details.join(' ')}` : message;
123
+ }
124
+ async function readControlPlaneError(response, fallback) {
125
+ const responseBody = await response.text();
126
+ const parsedBody = tryParseJson(responseBody);
127
+ return {
128
+ responseBody,
129
+ parsedBody,
130
+ message: getControlPlaneErrorMessage(parsedBody, fallback),
131
+ };
132
+ }
133
+ function tryParseJson(value) {
134
+ if (value.length === 0) {
135
+ return undefined;
136
+ }
137
+ try {
138
+ return JSON.parse(value);
139
+ }
140
+ catch {
141
+ return undefined;
142
+ }
143
+ }
144
+ function describeRequestBodyType(body) {
145
+ if (body === undefined || body === null) {
146
+ return 'empty';
147
+ }
148
+ if (typeof body === 'string') {
149
+ return 'string';
150
+ }
151
+ if (body instanceof URLSearchParams) {
152
+ return 'URLSearchParams';
153
+ }
154
+ if (typeof body === 'object' && 'constructor' in body) {
155
+ const constructorName = body.constructor?.name;
156
+ if (typeof constructorName === 'string' && constructorName.length > 0) {
157
+ return constructorName;
158
+ }
159
+ }
160
+ return typeof body;
161
+ }
162
+ /** Returns true when a request body can be replayed exactly across paid flows. */
163
+ export function isReplayableRequestBody(body) {
164
+ return typeof body === 'string' || body instanceof URLSearchParams;
165
+ }
166
+ /** Serialize a paid-flow request body into the exact replayable wire representation. */
167
+ export function toReplayableRequestBody(body) {
34
168
  if (body === undefined || body === null) {
35
169
  return undefined;
36
170
  }
@@ -40,7 +174,30 @@ function getReplayableRequestBody(body) {
40
174
  if (body instanceof URLSearchParams) {
41
175
  return body.toString();
42
176
  }
43
- throw new Error('Paid requests currently support replayable string and URLSearchParams bodies when routed through the control plane.');
177
+ throw new Error(`Paid requests require replayable string or URLSearchParams bodies. Received ${describeRequestBodyType(body)}. Convert JSON payloads with createJsonRequestBody(...) or form payloads with createFormUrlEncodedBody(...).`);
178
+ }
179
+ /** Helper for callers that want an explicit JSON-string body for paid flows. */
180
+ export function createJsonRequestBody(payload) {
181
+ return JSON.stringify(payload);
182
+ }
183
+ /** Helper for callers that want a replayable form body for paid flows. */
184
+ export function createFormUrlEncodedBody(values) {
185
+ const params = new URLSearchParams();
186
+ for (const [key, rawValue] of Object.entries(values)) {
187
+ if (Array.isArray(rawValue)) {
188
+ for (const entry of rawValue) {
189
+ params.append(key, String(entry));
190
+ }
191
+ continue;
192
+ }
193
+ params.append(key, String(rawValue));
194
+ }
195
+ return params;
196
+ }
197
+ // Preparation and paid execution may need to replay the exact request body, so
198
+ // only body types that can be losslessly serialized are accepted here.
199
+ function getReplayableRequestBody(body) {
200
+ return toReplayableRequestBody(body);
44
201
  }
45
202
  function hashRequestBody(body) {
46
203
  if (!body) {
@@ -65,9 +222,687 @@ function parseRuntimeTokenResponse(payload) {
65
222
  expiresAt,
66
223
  };
67
224
  }
225
+ function createPreparedHttpRequest(input, init) {
226
+ const requestBody = getReplayableRequestBody(init.body);
227
+ return paidRequestHttpRequestSchema.parse({
228
+ url: input,
229
+ method: (init.method ?? 'GET').toUpperCase(),
230
+ headers: normalizeHeaders(init.headers),
231
+ body: requestBody,
232
+ bodyHash: hashRequestBody(requestBody),
233
+ });
234
+ }
235
+ function createPreparationAttribution(source, authority, note) {
236
+ return {
237
+ source,
238
+ authority,
239
+ ...(note ? { note } : {}),
240
+ };
241
+ }
242
+ function readExternalMetadata(options) {
243
+ return options.externalMetadata;
244
+ }
245
+ function readSchemaType(value) {
246
+ if (typeof value === 'string' && value.length > 0) {
247
+ return value;
248
+ }
249
+ if (Array.isArray(value)) {
250
+ const firstString = value.find((entry) => typeof entry === 'string' && entry.length > 0);
251
+ if (firstString) {
252
+ return firstString;
253
+ }
254
+ }
255
+ return undefined;
256
+ }
257
+ function readMerchantFieldsFromObjectSchema(objectSchema) {
258
+ const properties = isRecord(objectSchema.properties)
259
+ ? objectSchema.properties
260
+ : undefined;
261
+ const required = Array.isArray(objectSchema.required)
262
+ ? new Set(objectSchema.required.filter((entry) => typeof entry === 'string' && entry.length > 0))
263
+ : new Set();
264
+ const fields = new Map();
265
+ for (const [name, schema] of Object.entries(properties ?? {})) {
266
+ const propertySchema = isRecord(schema) ? schema : {};
267
+ fields.set(name.toLowerCase(), {
268
+ name,
269
+ ...(readSchemaType(propertySchema.type) ? { type: readSchemaType(propertySchema.type) } : {}),
270
+ ...(readStringValue(propertySchema.description)
271
+ ? { description: readStringValue(propertySchema.description) }
272
+ : {}),
273
+ ...(required.has(name) ? { required: true } : {}),
274
+ });
275
+ }
276
+ for (const fieldName of required) {
277
+ if (!fields.has(fieldName.toLowerCase())) {
278
+ fields.set(fieldName.toLowerCase(), {
279
+ name: fieldName,
280
+ required: true,
281
+ });
282
+ }
283
+ }
284
+ return Array.from(fields.values());
285
+ }
286
+ function inferFieldTypeFromValue(value) {
287
+ if (Array.isArray(value)) {
288
+ return 'array';
289
+ }
290
+ if (value === null) {
291
+ return undefined;
292
+ }
293
+ switch (typeof value) {
294
+ case 'string':
295
+ return 'string';
296
+ case 'number':
297
+ return Number.isInteger(value) ? 'integer' : 'number';
298
+ case 'boolean':
299
+ return 'boolean';
300
+ case 'object':
301
+ return 'object';
302
+ default:
303
+ return undefined;
304
+ }
305
+ }
306
+ function inferFieldsFromQueryParamExample(queryParams) {
307
+ return Object.entries(queryParams).map(([name, value]) => ({
308
+ name,
309
+ ...(inferFieldTypeFromValue(value) ? { type: inferFieldTypeFromValue(value) } : {}),
310
+ required: true,
311
+ }));
312
+ }
313
+ async function resolveJsonSchemaRef(schema, fetchImpl) {
314
+ const ref = readStringValue(schema.$ref);
315
+ if (!ref) {
316
+ return schema;
317
+ }
318
+ try {
319
+ const url = new URL(ref);
320
+ if (!['http:', 'https:'].includes(url.protocol)) {
321
+ return undefined;
322
+ }
323
+ const response = await fetchImpl(url.toString());
324
+ if (!response.ok) {
325
+ return undefined;
326
+ }
327
+ const parsed = await response.json();
328
+ return isRecord(parsed) ? parsed : undefined;
329
+ }
330
+ catch {
331
+ return undefined;
332
+ }
333
+ }
334
+ async function readMerchantPreparationMetadataFromInputSchema(inputSchema, requestBodyExample, queryParamExample, fetchImpl) {
335
+ const bodyType = readStringValue(inputSchema.bodyType)
336
+ ?? (isRecord(inputSchema.properties)
337
+ && isRecord(inputSchema.properties.bodyType)
338
+ ? readStringValue(inputSchema.properties.bodyType.const)
339
+ : undefined);
340
+ const bodySchema = isRecord(inputSchema.body)
341
+ ? inputSchema.body
342
+ : isRecord(inputSchema.properties)
343
+ && isRecord(inputSchema.properties.body)
344
+ ? inputSchema.properties.body
345
+ : undefined;
346
+ const queryParamSchema = isRecord(inputSchema.queryParams)
347
+ ? inputSchema.queryParams
348
+ : isRecord(inputSchema.properties)
349
+ && isRecord(inputSchema.properties.queryParams)
350
+ ? inputSchema.properties.queryParams
351
+ : undefined;
352
+ const requestBodyFields = bodySchema
353
+ ? readMerchantFieldsFromObjectSchema(bodySchema)
354
+ : undefined;
355
+ const resolvedQueryParamSchema = queryParamSchema
356
+ ? await resolveJsonSchemaRef(queryParamSchema, fetchImpl)
357
+ : undefined;
358
+ const requestQueryParams = resolvedQueryParamSchema
359
+ ? readMerchantFieldsFromObjectSchema(resolvedQueryParamSchema)
360
+ : queryParamExample
361
+ ? inferFieldsFromQueryParamExample(queryParamExample)
362
+ : undefined;
363
+ const hasBodyFields = requestBodyFields && requestBodyFields.length > 0;
364
+ const hasQueryFields = requestQueryParams && requestQueryParams.length > 0;
365
+ if (!bodyType && !requestBodyExample && !hasBodyFields && !hasQueryFields) {
366
+ return undefined;
367
+ }
368
+ return {
369
+ ...(bodyType ? { requestBodyType: bodyType } : {}),
370
+ ...(requestBodyExample ? { requestBodyExample } : {}),
371
+ ...(hasBodyFields
372
+ ? { requestBodyFields }
373
+ : {}),
374
+ ...(hasQueryFields
375
+ ? { requestQueryParams }
376
+ : {}),
377
+ notes: ['Request hints derived from merchant challenge metadata.'],
378
+ };
379
+ }
380
+ async function readMerchantPreparationMetadata(challenge, fetchImpl) {
381
+ if (!challenge) {
382
+ return undefined;
383
+ }
384
+ const paymentRequiredPayload = tryParsePaymentRequiredHeader(challenge.headers['payment-required']);
385
+ const payload = unwrapChallengePayload(paymentRequiredPayload ?? challenge.body);
386
+ if (!isRecord(payload)) {
387
+ return undefined;
388
+ }
389
+ const accepts = Array.isArray(payload.accepts) ? payload.accepts : [];
390
+ for (const accept of accepts) {
391
+ if (!isRecord(accept) || !isRecord(accept.extra) || !isRecord(accept.extra.outputSchema)) {
392
+ continue;
393
+ }
394
+ const outputSchema = accept.extra.outputSchema;
395
+ const metadata = isRecord(outputSchema.input)
396
+ ? await readMerchantPreparationMetadataFromInputSchema(outputSchema.input, undefined, undefined, fetchImpl)
397
+ : undefined;
398
+ if (metadata) {
399
+ return metadata;
400
+ }
401
+ }
402
+ const bazaar = isRecord(payload.extensions) && isRecord(payload.extensions.bazaar)
403
+ ? payload.extensions.bazaar
404
+ : undefined;
405
+ const bazaarInfoInput = isRecord(bazaar?.info) && isRecord(bazaar.info.input)
406
+ ? bazaar.info.input
407
+ : undefined;
408
+ const bazaarRequestBodyExample = isRecord(bazaarInfoInput?.body)
409
+ ? JSON.stringify(bazaarInfoInput.body)
410
+ : undefined;
411
+ const bazaarQueryParamExample = isRecord(bazaarInfoInput?.queryParams)
412
+ ? bazaarInfoInput.queryParams
413
+ : undefined;
414
+ const bazaarInputSchema = isRecord(bazaar?.schema)
415
+ && isRecord(bazaar.schema.properties)
416
+ && isRecord(bazaar.schema.properties.input)
417
+ ? bazaar.schema.properties.input
418
+ : undefined;
419
+ if (bazaarInputSchema) {
420
+ return readMerchantPreparationMetadataFromInputSchema(bazaarInputSchema, bazaarRequestBodyExample, bazaarQueryParamExample, fetchImpl);
421
+ }
422
+ return undefined;
423
+ }
424
+ function pickPreparedHintValue(options, merchantChallengeMetadata, key) {
425
+ if (merchantChallengeMetadata?.[key]) {
426
+ return {
427
+ value: merchantChallengeMetadata[key],
428
+ attribution: createPreparationAttribution('merchant_challenge', 'authoritative'),
429
+ };
430
+ }
431
+ const externalMetadata = readExternalMetadata(options);
432
+ if (externalMetadata?.[key]) {
433
+ return {
434
+ value: externalMetadata[key],
435
+ attribution: createPreparationAttribution('external_metadata', 'advisory'),
436
+ };
437
+ }
438
+ return undefined;
439
+ }
440
+ function mergePreparedHintFields(options, merchantChallengeMetadata, key) {
441
+ const fields = new Map();
442
+ for (const field of merchantChallengeMetadata?.[key] ?? []) {
443
+ fields.set(field.name.toLowerCase(), {
444
+ ...field,
445
+ attribution: createPreparationAttribution('merchant_challenge', 'authoritative'),
446
+ });
447
+ }
448
+ for (const field of readExternalMetadata(options)?.[key] ?? []) {
449
+ const normalizedName = field.name.toLowerCase();
450
+ if (fields.has(normalizedName)) {
451
+ continue;
452
+ }
453
+ fields.set(normalizedName, {
454
+ ...field,
455
+ attribution: createPreparationAttribution('external_metadata', 'advisory'),
456
+ });
457
+ }
458
+ return Array.from(fields.values());
459
+ }
460
+ async function buildPreparedRequestHints(options, fetchImpl, challenge) {
461
+ // Hint assembly merges optional caller metadata with merchant-authoritative
462
+ // challenge metadata while preserving attribution for every returned field.
463
+ const merchantChallengeMetadata = await readMerchantPreparationMetadata(challenge, fetchImpl);
464
+ const externalMetadata = readExternalMetadata(options);
465
+ return {
466
+ ...(pickPreparedHintValue(options, merchantChallengeMetadata, 'description')
467
+ ? {
468
+ description: pickPreparedHintValue(options, merchantChallengeMetadata, 'description'),
469
+ }
470
+ : {}),
471
+ ...(pickPreparedHintValue(options, merchantChallengeMetadata, 'requestBodyType')
472
+ ? {
473
+ requestBodyType: pickPreparedHintValue(options, merchantChallengeMetadata, 'requestBodyType'),
474
+ }
475
+ : {}),
476
+ ...(pickPreparedHintValue(options, merchantChallengeMetadata, 'requestBodyExample')
477
+ ? {
478
+ requestBodyExample: pickPreparedHintValue(options, merchantChallengeMetadata, 'requestBodyExample'),
479
+ }
480
+ : {}),
481
+ requestBodyFields: mergePreparedHintFields(options, merchantChallengeMetadata, 'requestBodyFields'),
482
+ requestQueryParams: mergePreparedHintFields(options, merchantChallengeMetadata, 'requestQueryParams'),
483
+ requestPathParams: mergePreparedHintFields(options, merchantChallengeMetadata, 'requestPathParams'),
484
+ notes: [
485
+ ...(merchantChallengeMetadata?.notes ?? []).map((value) => ({
486
+ value,
487
+ attribution: createPreparationAttribution('merchant_challenge', 'authoritative'),
488
+ })),
489
+ ...(externalMetadata?.notes ?? []).map((value) => ({
490
+ value,
491
+ attribution: createPreparationAttribution('external_metadata', 'advisory'),
492
+ })),
493
+ ],
494
+ };
495
+ }
496
+ function isJsonContentType(value) {
497
+ return value?.toLowerCase().includes('application/json') ?? false;
498
+ }
499
+ function matchesExpectedFieldType(value, expectedType) {
500
+ switch (expectedType?.toLowerCase()) {
501
+ case 'string':
502
+ return typeof value === 'string';
503
+ case 'number':
504
+ return typeof value === 'number' && Number.isFinite(value);
505
+ case 'integer':
506
+ case 'int':
507
+ return typeof value === 'number' && Number.isInteger(value);
508
+ case 'boolean':
509
+ return typeof value === 'boolean';
510
+ case 'object':
511
+ return isRecord(value);
512
+ case 'array':
513
+ return Array.isArray(value);
514
+ default:
515
+ return true;
516
+ }
517
+ }
518
+ function buildPreparedValidationIssues(request, hints) {
519
+ // Validation is intentionally narrow: it checks only request-shape issues the
520
+ // SDK can defend from available hints, not task-specific semantic correctness.
521
+ const issues = [];
522
+ const contentType = request.headers?.['content-type'];
523
+ const requestBodyType = hints.requestBodyType?.value.toLowerCase();
524
+ const requiredBodyFields = hints.requestBodyFields.filter((field) => field.required);
525
+ if (requestBodyType === 'json'
526
+ && request.body !== undefined
527
+ && !isJsonContentType(contentType)) {
528
+ issues.push({
529
+ location: 'headers',
530
+ field: 'content-type',
531
+ code: 'unsupported_content_type',
532
+ message: 'Request body is expected to be JSON but content-type is not application/json.',
533
+ source: hints.requestBodyType?.attribution.source ?? 'external_metadata',
534
+ blocking: true,
535
+ severity: 'error',
536
+ suggestedFix: 'Send the request with content-type: application/json.',
537
+ });
538
+ }
539
+ if (hints.requestBodyFields.length > 0) {
540
+ if (request.body === undefined) {
541
+ for (const field of requiredBodyFields) {
542
+ issues.push({
543
+ location: 'body',
544
+ field: field.name,
545
+ code: 'missing_required_field',
546
+ message: `Required request body field "${field.name}" is missing.`,
547
+ source: field.attribution.source,
548
+ blocking: true,
549
+ severity: 'error',
550
+ suggestedFix: `Add the required body field "${field.name}" before execution.`,
551
+ });
552
+ }
553
+ }
554
+ else {
555
+ const parsedBody = tryParseJson(request.body);
556
+ if (parsedBody === undefined) {
557
+ issues.push({
558
+ location: 'body',
559
+ field: 'body',
560
+ code: 'malformed_candidate_value',
561
+ message: 'Request body must be valid JSON to satisfy the declared body fields.',
562
+ source: hints.requestBodyType?.attribution.source
563
+ ?? hints.requestBodyFields[0]?.attribution.source
564
+ ?? 'external_metadata',
565
+ blocking: true,
566
+ severity: 'error',
567
+ suggestedFix: 'Send a valid JSON object body that satisfies the declared fields.',
568
+ });
569
+ }
570
+ else if (!isRecord(parsedBody)) {
571
+ issues.push({
572
+ location: 'body',
573
+ field: 'body',
574
+ code: 'request_shape_conflicts_with_hint',
575
+ message: 'Request body must be a JSON object to satisfy the declared body fields.',
576
+ source: hints.requestBodyFields[0]?.attribution.source
577
+ ?? hints.requestBodyType?.attribution.source
578
+ ?? 'external_metadata',
579
+ blocking: true,
580
+ severity: 'error',
581
+ suggestedFix: 'Send a JSON object body whose keys match the declared fields.',
582
+ });
583
+ }
584
+ else {
585
+ for (const field of requiredBodyFields) {
586
+ if (!(field.name in parsedBody)) {
587
+ issues.push({
588
+ location: 'body',
589
+ field: field.name,
590
+ code: 'missing_required_field',
591
+ message: `Required request body field "${field.name}" is missing.`,
592
+ source: field.attribution.source,
593
+ blocking: true,
594
+ severity: 'error',
595
+ suggestedFix: `Add the required body field "${field.name}" before execution.`,
596
+ });
597
+ }
598
+ }
599
+ for (const field of hints.requestBodyFields) {
600
+ if (!(field.name in parsedBody)) {
601
+ continue;
602
+ }
603
+ if (!matchesExpectedFieldType(parsedBody[field.name], field.type)) {
604
+ issues.push({
605
+ location: 'body',
606
+ field: field.name,
607
+ code: 'malformed_candidate_value',
608
+ message: `Request body field "${field.name}" does not match the expected type${field.type ? ` "${field.type}"` : ''}.`,
609
+ source: field.attribution.source,
610
+ blocking: true,
611
+ severity: 'error',
612
+ suggestedFix: `Set "${field.name}" to a value compatible with the declared type${field.type ? ` "${field.type}"` : ''}.`,
613
+ });
614
+ }
615
+ }
616
+ }
617
+ }
618
+ }
619
+ const url = new URL(request.url);
620
+ for (const field of hints.requestQueryParams.filter((entry) => entry.required)) {
621
+ if (!url.searchParams.has(field.name)) {
622
+ issues.push({
623
+ location: 'query',
624
+ field: field.name,
625
+ code: 'missing_required_query_param',
626
+ message: `Required query parameter "${field.name}" is missing.`,
627
+ source: field.attribution.source,
628
+ blocking: true,
629
+ severity: 'error',
630
+ suggestedFix: `Add the required query parameter "${field.name}" before execution.`,
631
+ });
632
+ }
633
+ }
634
+ for (const field of hints.requestPathParams.filter((entry) => entry.required)) {
635
+ if (url.pathname.includes(`{${field.name}}`)
636
+ || url.pathname.includes(`:${field.name}`)) {
637
+ issues.push({
638
+ location: 'path',
639
+ field: field.name,
640
+ code: 'missing_required_path_param',
641
+ message: `Required path parameter "${field.name}" is still unresolved in the request URL.`,
642
+ source: field.attribution.source,
643
+ blocking: true,
644
+ severity: 'error',
645
+ suggestedFix: `Replace the unresolved path placeholder for "${field.name}" before execution.`,
646
+ });
647
+ }
648
+ }
649
+ const seen = new Set();
650
+ return issues.filter((issue) => {
651
+ const key = [
652
+ issue.location,
653
+ issue.field.toLowerCase(),
654
+ issue.code,
655
+ issue.source,
656
+ ].join(':');
657
+ if (seen.has(key)) {
658
+ return false;
659
+ }
660
+ seen.add(key);
661
+ return true;
662
+ });
663
+ }
664
+ function derivePreparedNextAction(kind, validationIssues) {
665
+ if (kind === 'passthrough') {
666
+ return 'treat_as_passthrough';
667
+ }
668
+ if (validationIssues.some((issue) => issue.blocking)) {
669
+ return 'revise_request';
670
+ }
671
+ return 'execute';
672
+ }
673
+ function readStringValue(value) {
674
+ return typeof value === 'string' && value.length > 0 ? value : undefined;
675
+ }
676
+ function readIntegerValue(value) {
677
+ if (typeof value === 'number' && Number.isInteger(value)) {
678
+ return value;
679
+ }
680
+ if (typeof value === 'string' && /^\d+$/.test(value)) {
681
+ return Number.parseInt(value, 10);
682
+ }
683
+ return undefined;
684
+ }
685
+ function readPrecisionFromAmount(amount) {
686
+ if (!amount) {
687
+ return undefined;
688
+ }
689
+ const [, fractionalPart = ''] = amount.split('.');
690
+ return fractionalPart.length;
691
+ }
692
+ function formatMinorUnitsAsAmount(amountMinor, precision) {
693
+ if (precision === 0) {
694
+ return amountMinor;
695
+ }
696
+ const normalizedMinor = amountMinor.replace(/^0+(?=\d)/, '') || '0';
697
+ const paddedMinor = normalizedMinor.padStart(precision + 1, '0');
698
+ const wholePart = paddedMinor.slice(0, -precision) || '0';
699
+ const fractionalPart = paddedMinor.slice(-precision);
700
+ return `${wholePart}.${fractionalPart}`;
701
+ }
702
+ function tryParsePaymentRequiredHeader(value) {
703
+ if (!value) {
704
+ return undefined;
705
+ }
706
+ try {
707
+ return JSON.parse(Buffer.from(value, 'base64').toString('utf8'));
708
+ }
709
+ catch {
710
+ // Fall through.
711
+ }
712
+ return tryParseJson(value);
713
+ }
714
+ function unwrapChallengePayload(payload) {
715
+ if (!isRecord(payload)) {
716
+ return undefined;
717
+ }
718
+ if (isRecord(payload.challenge)) {
719
+ return payload.challenge;
720
+ }
721
+ return payload;
722
+ }
723
+ function parseAuthenticateHeaderParameters(header) {
724
+ if (!header) {
725
+ return new Map();
726
+ }
727
+ const matches = header.matchAll(/([a-zA-Z0-9_-]+)="([^"]+)"/g);
728
+ const parameters = new Map();
729
+ for (const match of matches) {
730
+ const [, key, value] = match;
731
+ if (key && value) {
732
+ parameters.set(key.toLowerCase(), value);
733
+ }
734
+ }
735
+ return parameters;
736
+ }
737
+ function buildPreparedPaymentRequirement(challenge) {
738
+ // Payment terms are normalized into one portable structure so callers do not
739
+ // need protocol-specific parsing logic in their agent or application layer.
740
+ const provenance = createPreparationAttribution('merchant_challenge', 'authoritative');
741
+ const paymentRequiredPayload = tryParsePaymentRequiredHeader(challenge.headers['payment-required']);
742
+ const payload = unwrapChallengePayload(paymentRequiredPayload ?? challenge.body);
743
+ if (isRecord(payload)) {
744
+ const resource = isRecord(payload.resource) ? payload.resource : undefined;
745
+ const accepts = Array.isArray(payload.accepts) ? payload.accepts : undefined;
746
+ const primaryAccept = accepts?.find((candidate) => isRecord(candidate));
747
+ if (primaryAccept && isRecord(primaryAccept)) {
748
+ const amountMinor = readStringValue(primaryAccept.amount)
749
+ ?? readStringValue(primaryAccept.maxAmountRequired);
750
+ const precision = readIntegerValue(primaryAccept.precision)
751
+ ?? (isRecord(primaryAccept.extra)
752
+ ? readIntegerValue(primaryAccept.extra.precision)
753
+ ?? readIntegerValue(primaryAccept.extra.decimals)
754
+ : undefined);
755
+ const amount = amountMinor && precision !== undefined
756
+ ? formatMinorUnitsAsAmount(amountMinor, precision)
757
+ : undefined;
758
+ return {
759
+ protocol: challenge.protocol,
760
+ ...(readStringValue(resource?.description)
761
+ ? { description: readStringValue(resource?.description) }
762
+ : {}),
763
+ ...(readStringValue(primaryAccept.asset)
764
+ ? { asset: readStringValue(primaryAccept.asset) }
765
+ : {}),
766
+ ...(readStringValue(primaryAccept.network)
767
+ ? { network: readStringValue(primaryAccept.network) }
768
+ : {}),
769
+ ...(readStringValue(primaryAccept.payTo)
770
+ ? { payee: readStringValue(primaryAccept.payTo) }
771
+ : {}),
772
+ ...(readStringValue(primaryAccept.amount)
773
+ ? { amountType: 'exact' }
774
+ : readStringValue(primaryAccept.maxAmountRequired)
775
+ ? { amountType: 'max' }
776
+ : {}),
777
+ ...(amount ? { amount } : {}),
778
+ ...(amountMinor ? { amountMinor } : {}),
779
+ ...(precision !== undefined ? { precision } : {}),
780
+ provenance,
781
+ };
782
+ }
783
+ }
784
+ const explicitAmount = readStringValue(challenge.headers['x-payment-amount']);
785
+ const explicitPrecision = readIntegerValue(challenge.headers['x-payment-precision'])
786
+ ?? readPrecisionFromAmount(explicitAmount);
787
+ if (explicitAmount || challenge.headers['www-authenticate']) {
788
+ const authenticateParameters = parseAuthenticateHeaderParameters(challenge.headers['www-authenticate']);
789
+ const amount = explicitAmount ?? authenticateParameters.get('amount');
790
+ const precision = explicitPrecision ?? readPrecisionFromAmount(amount);
791
+ const asset = readStringValue(challenge.headers['x-payment-asset'])
792
+ ?? authenticateParameters.get('asset');
793
+ const payee = readStringValue(challenge.headers['x-payment-payee'])
794
+ ?? authenticateParameters.get('payee');
795
+ const network = readStringValue(challenge.headers['x-payment-network'])
796
+ ?? authenticateParameters.get('network');
797
+ const amountMinor = amount && precision !== undefined
798
+ ? monetaryAmountToMinorUnits(amount, precision)
799
+ : undefined;
800
+ return {
801
+ protocol: challenge.protocol,
802
+ ...(asset ? { asset } : {}),
803
+ ...(network ? { network } : {}),
804
+ ...(payee ? { payee } : {}),
805
+ ...(amount ? { amount } : {}),
806
+ ...(amountMinor ? { amountMinor } : {}),
807
+ ...(precision !== undefined ? { precision } : {}),
808
+ ...(amount ? { amountType: 'exact' } : {}),
809
+ provenance,
810
+ };
811
+ }
812
+ return {
813
+ protocol: challenge.protocol,
814
+ provenance,
815
+ };
816
+ }
817
+ function buildPreparedChallengeAccept(candidate) {
818
+ const maxTimeoutSeconds = readIntegerValue(candidate.maxTimeoutSeconds);
819
+ return {
820
+ ...(readStringValue(candidate.scheme)
821
+ ? { scheme: readStringValue(candidate.scheme) }
822
+ : {}),
823
+ ...(readStringValue(candidate.network)
824
+ ? { network: readStringValue(candidate.network) }
825
+ : {}),
826
+ ...(readStringValue(candidate.amount)
827
+ ? { amount: readStringValue(candidate.amount) }
828
+ : {}),
829
+ ...(readStringValue(candidate.maxAmountRequired)
830
+ ? { maxAmountRequired: readStringValue(candidate.maxAmountRequired) }
831
+ : {}),
832
+ ...(readStringValue(candidate.asset)
833
+ ? { asset: readStringValue(candidate.asset) }
834
+ : {}),
835
+ ...(readStringValue(candidate.payTo)
836
+ ? { payTo: readStringValue(candidate.payTo) }
837
+ : {}),
838
+ ...(maxTimeoutSeconds && maxTimeoutSeconds > 0
839
+ ? { maxTimeoutSeconds }
840
+ : {}),
841
+ ...(readStringValue(candidate.resource)
842
+ ? { resource: readStringValue(candidate.resource) }
843
+ : {}),
844
+ ...(readStringValue(candidate.description)
845
+ ? { description: readStringValue(candidate.description) }
846
+ : {}),
847
+ ...(readStringValue(candidate.mimeType)
848
+ ? { mimeType: readStringValue(candidate.mimeType) }
849
+ : {}),
850
+ ...(candidate.outputSchema !== undefined
851
+ ? { outputSchema: candidate.outputSchema }
852
+ : {}),
853
+ ...(isRecord(candidate.extra)
854
+ ? { extra: candidate.extra }
855
+ : {}),
856
+ };
857
+ }
858
+ function buildPreparedChallengeDetails(challenge) {
859
+ if (challenge.protocol !== 'x402') {
860
+ return undefined;
861
+ }
862
+ const paymentRequiredPayload = tryParsePaymentRequiredHeader(challenge.headers['payment-required']);
863
+ const payload = unwrapChallengePayload(paymentRequiredPayload ?? challenge.body);
864
+ if (!isRecord(payload)) {
865
+ return undefined;
866
+ }
867
+ const resource = isRecord(payload.resource) ? payload.resource : undefined;
868
+ const accepts = Array.isArray(payload.accepts)
869
+ ? payload.accepts.filter((candidate) => isRecord(candidate))
870
+ : [];
871
+ const x402Version = readIntegerValue(payload.x402Version);
872
+ return {
873
+ ...(x402Version !== undefined ? { x402Version } : {}),
874
+ ...(readStringValue(payload.error)
875
+ ? { error: readStringValue(payload.error) }
876
+ : {}),
877
+ ...(resource && readStringValue(resource.url)
878
+ ? {
879
+ resource: {
880
+ url: readStringValue(resource.url),
881
+ ...(readStringValue(resource.description)
882
+ ? { description: readStringValue(resource.description) }
883
+ : {}),
884
+ ...(readStringValue(resource.mimeType)
885
+ ? { mimeType: readStringValue(resource.mimeType) }
886
+ : {}),
887
+ },
888
+ }
889
+ : {}),
890
+ accepts: accepts.map((candidate) => buildPreparedChallengeAccept(candidate)),
891
+ ...(isRecord(payload.extensions)
892
+ ? { extensions: payload.extensions }
893
+ : {}),
894
+ };
895
+ }
896
+ /**
897
+ * Client bound to one organization/agent identity and one 402flow control plane.
898
+ *
899
+ * Most integrations either call fetchPaid() directly or use the explicit
900
+ * preparePaidRequest() -> executePreparedRequest() flow.
901
+ */
68
902
  export class AgentPayClient {
69
903
  controlPlaneBaseUrl;
70
904
  auth;
905
+ identity;
71
906
  fetchImpl;
72
907
  headers;
73
908
  cachedRuntimeToken;
@@ -75,11 +910,97 @@ export class AgentPayClient {
75
910
  constructor(options) {
76
911
  this.controlPlaneBaseUrl = trimTrailingSlash(options.controlPlaneBaseUrl);
77
912
  this.auth = options.auth;
913
+ this.identity = {
914
+ organization: options.organization,
915
+ agent: options.agent,
916
+ };
78
917
  this.fetchImpl = options.fetch ?? fetch;
79
918
  this.headers = options.headers;
80
919
  }
81
- async fetchPaid(input, init = {}, context, options) {
82
- let challenge = options.challenge;
920
+ /**
921
+ * Probe or reuse a merchant challenge and return a normalized preparation
922
+ * result the caller can inspect before paying.
923
+ */
924
+ async preparePaidRequest(input, init = {}, options = {}) {
925
+ const request = createPreparedHttpRequest(input, init);
926
+ if (options.challenge) {
927
+ const hints = await buildPreparedRequestHints(options, this.fetchImpl, options.challenge);
928
+ const validationIssues = buildPreparedValidationIssues(request, hints);
929
+ const paymentRequirement = buildPreparedPaymentRequirement(options.challenge);
930
+ const challengeDetails = buildPreparedChallengeDetails(options.challenge);
931
+ return {
932
+ kind: 'ready',
933
+ protocol: options.challenge.protocol,
934
+ request,
935
+ challenge: paidRequestChallengeSchema.parse(options.challenge),
936
+ ...(challengeDetails ? { challengeDetails } : {}),
937
+ ...(paymentRequirement
938
+ ? {
939
+ paymentRequirement,
940
+ }
941
+ : {}),
942
+ hints,
943
+ validationIssues,
944
+ nextAction: derivePreparedNextAction('ready', validationIssues),
945
+ };
946
+ }
947
+ const initialResponse = await this.fetchImpl(input, init);
948
+ const challenge = await detectChallengeFromResponse(initialResponse);
949
+ const probe = {
950
+ responseStatus: initialResponse.status,
951
+ confirmedAt: new Date().toISOString(),
952
+ };
953
+ const hints = await buildPreparedRequestHints(options, this.fetchImpl, challenge);
954
+ const validationIssues = buildPreparedValidationIssues(request, hints);
955
+ if (!challenge) {
956
+ return {
957
+ kind: 'passthrough',
958
+ protocol: 'none',
959
+ request,
960
+ hints,
961
+ probe,
962
+ validationIssues,
963
+ nextAction: derivePreparedNextAction('passthrough', validationIssues),
964
+ };
965
+ }
966
+ const paymentRequirement = buildPreparedPaymentRequirement(challenge);
967
+ const challengeDetails = buildPreparedChallengeDetails(challenge);
968
+ return {
969
+ kind: 'ready',
970
+ protocol: challenge.protocol,
971
+ request,
972
+ challenge: paidRequestChallengeSchema.parse(challenge),
973
+ ...(challengeDetails ? { challengeDetails } : {}),
974
+ ...(paymentRequirement
975
+ ? { paymentRequirement }
976
+ : {}),
977
+ hints,
978
+ probe,
979
+ validationIssues,
980
+ nextAction: derivePreparedNextAction('ready', validationIssues),
981
+ };
982
+ }
983
+ /**
984
+ * Execute the exact request that was previously prepared, without re-probing
985
+ * the merchant first.
986
+ */
987
+ async executePreparedRequest(prepared, request = {}) {
988
+ return this.fetchPaid(prepared.request.url, {
989
+ method: prepared.request.method,
990
+ ...(prepared.request.headers ? { headers: prepared.request.headers } : {}),
991
+ ...(prepared.request.body !== undefined ? { body: prepared.request.body } : {}),
992
+ }, {
993
+ ...request,
994
+ challenge: prepared.challenge,
995
+ });
996
+ }
997
+ /**
998
+ * Fast-path helper that probes when needed, asks the control plane for a paid
999
+ * execution decision, and returns either passthrough or success. All non-success
1000
+ * paid outcomes are thrown as FetchPaidError.
1001
+ */
1002
+ async fetchPaid(input, init = {}, request) {
1003
+ let challenge = request.challenge;
83
1004
  if (!challenge) {
84
1005
  const initialResponse = await this.fetchImpl(input, init);
85
1006
  challenge = await detectChallengeFromResponse(initialResponse);
@@ -91,41 +1012,41 @@ export class AgentPayClient {
91
1012
  };
92
1013
  }
93
1014
  }
94
- const decisionRequest = await this.createDecisionRequest(input, init, context, options, challenge);
95
- const decision = await this.requestPaymentDecision(decisionRequest);
1015
+ const decisionRequest = this.createDecisionRequest(input, init, request, challenge);
1016
+ const decision = await this.requestPaymentDecision(decisionRequest, challenge.protocol);
96
1017
  return this.mapDecisionToPaidResponse(decision, challenge.protocol);
97
1018
  }
1019
+ /** Lookup a durable receipt by id through the control plane. */
98
1020
  async lookupReceipt(receiptId) {
99
1021
  const response = await this.controlPlaneFetch(`/api/sdk/receipts/${receiptId}`, {
100
1022
  method: 'GET',
101
1023
  }, await this.getRuntimeAuthorizationHeader());
102
1024
  if (!response.ok) {
103
- throw new Error(`Receipt lookup failed with status ${response.status}.`);
1025
+ const error = await readControlPlaneError(response, `Receipt lookup failed with status ${response.status}.`);
1026
+ throw new Error(error.message);
104
1027
  }
105
1028
  return sdkReceiptResponseSchema.parse(await response.json());
106
1029
  }
107
- async createDecisionRequest(input, init, context, options, challenge) {
108
- const requestBody = getReplayableRequestBody(init.body);
1030
+ createDecisionRequest(input, init, request, challenge) {
1031
+ const { challenge: _challenge, idempotencyKey, ...requestContext } = request;
109
1032
  return sdkPaymentDecisionRequestSchema.parse({
110
- context,
111
- target: options.target,
112
- request: {
113
- url: input,
114
- method: (init.method ?? 'GET').toUpperCase(),
115
- headers: normalizeHeaders(init.headers),
116
- body: requestBody,
117
- bodyHash: hashRequestBody(requestBody),
1033
+ context: {
1034
+ ...this.identity,
1035
+ ...requestContext,
118
1036
  },
1037
+ request: createPreparedHttpRequest(input, init),
119
1038
  challenge: {
120
1039
  protocol: challenge.protocol,
121
- money: challenge.money,
122
- raw: challenge.raw,
123
- ...(challenge.payee ? { payee: challenge.payee } : {}),
1040
+ headers: challenge.headers,
1041
+ ...(challenge.body !== undefined ? { body: challenge.body } : {}),
124
1042
  },
125
- idempotencyKey: options.idempotencyKey,
1043
+ idempotencyKey,
126
1044
  });
127
1045
  }
128
- async requestPaymentDecision(decisionRequest) {
1046
+ async requestPaymentDecision(decisionRequest, protocol) {
1047
+ // The control plane is the single source of truth for payment policy and
1048
+ // receipt persistence. Any response that does not match the SDK contract is
1049
+ // downgraded to request_failed.
129
1050
  const decisionResponse = await this.controlPlaneFetch('/api/sdk/payment-decisions', {
130
1051
  method: 'POST',
131
1052
  headers: {
@@ -133,12 +1054,46 @@ export class AgentPayClient {
133
1054
  },
134
1055
  body: JSON.stringify(decisionRequest),
135
1056
  }, await this.getRuntimeAuthorizationHeader());
1057
+ const responseBody = await decisionResponse.text();
1058
+ const parsedBody = tryParseJson(responseBody);
1059
+ if (parsedBody !== undefined) {
1060
+ try {
1061
+ return sdkPaymentDecisionResponseSchema.parse(parsedBody);
1062
+ }
1063
+ catch {
1064
+ // Fall through to request-failed handling below.
1065
+ }
1066
+ }
136
1067
  if (!decisionResponse.ok) {
137
- throw new Error(`Payment decision failed with status ${decisionResponse.status}.`);
1068
+ throw new FetchPaidError({
1069
+ kind: 'request_failed',
1070
+ protocol,
1071
+ response: createRawResponse(decisionResponse.status, responseBody, decisionResponse.headers),
1072
+ reason: getControlPlaneErrorMessage(parsedBody, `Payment decision failed with status ${decisionResponse.status}.`),
1073
+ decision: {
1074
+ outcome: 'request_failed',
1075
+ status: decisionResponse.status,
1076
+ message: getControlPlaneErrorMessage(parsedBody, `Payment decision failed with status ${decisionResponse.status}.`),
1077
+ ...(parsedBody !== undefined ? { body: parsedBody } : {}),
1078
+ },
1079
+ });
138
1080
  }
139
- return sdkPaymentDecisionResponseSchema.parse(await decisionResponse.json());
1081
+ throw new FetchPaidError({
1082
+ kind: 'request_failed',
1083
+ protocol,
1084
+ response: createRawResponse(decisionResponse.status, responseBody, decisionResponse.headers),
1085
+ reason: 'Payment decision response did not match the SDK contract.',
1086
+ decision: {
1087
+ outcome: 'request_failed',
1088
+ status: decisionResponse.status,
1089
+ message: 'Payment decision response did not match the SDK contract.',
1090
+ ...(parsedBody !== undefined ? { body: parsedBody } : {}),
1091
+ },
1092
+ });
140
1093
  }
141
1094
  mapDecisionToPaidResponse(decision, protocol) {
1095
+ // Only allow returns normally. Every other durable or non-durable paid outcome
1096
+ // is surfaced as a typed FetchPaidError so caller control flow stays explicit.
142
1097
  switch (decision.outcome) {
143
1098
  case 'allow': {
144
1099
  const response = {
@@ -164,7 +1119,7 @@ export class AgentPayClient {
164
1119
  reason: decision.reason,
165
1120
  decision,
166
1121
  };
167
- return response;
1122
+ throw new FetchPaidError(response);
168
1123
  }
169
1124
  case 'deny': {
170
1125
  const response = {
@@ -180,7 +1135,7 @@ export class AgentPayClient {
180
1135
  ? { policyReviewEventId: decision.policyReviewEventId }
181
1136
  : {}),
182
1137
  };
183
- return response;
1138
+ throw new FetchPaidError(response);
184
1139
  }
185
1140
  case 'executing': {
186
1141
  const response = {
@@ -192,7 +1147,7 @@ export class AgentPayClient {
192
1147
  reason: decision.reason,
193
1148
  decision,
194
1149
  };
195
- return response;
1150
+ throw new FetchPaidError(response);
196
1151
  }
197
1152
  case 'inconclusive': {
198
1153
  const response = {
@@ -204,7 +1159,7 @@ export class AgentPayClient {
204
1159
  reason: decision.reason,
205
1160
  decision,
206
1161
  };
207
- return response;
1162
+ throw new FetchPaidError(response);
208
1163
  }
209
1164
  case 'execution_failed': {
210
1165
  const response = {
@@ -216,7 +1171,7 @@ export class AgentPayClient {
216
1171
  reason: decision.reason,
217
1172
  decision,
218
1173
  };
219
- return response;
1174
+ throw new FetchPaidError(response);
220
1175
  }
221
1176
  case 'preflight_failed': {
222
1177
  const response = {
@@ -228,7 +1183,7 @@ export class AgentPayClient {
228
1183
  reason: decision.reason,
229
1184
  decision,
230
1185
  };
231
- return response;
1186
+ throw new FetchPaidError(response);
232
1187
  }
233
1188
  }
234
1189
  }
@@ -264,7 +1219,8 @@ export class AgentPayClient {
264
1219
  method: 'POST',
265
1220
  }, `Bearer ${this.auth.bootstrapKey}`);
266
1221
  if (!response.ok) {
267
- throw new Error(`Runtime token exchange failed with status ${response.status}.`);
1222
+ const error = await readControlPlaneError(response, `Runtime token exchange failed with status ${response.status}.`);
1223
+ throw new Error(error.message);
268
1224
  }
269
1225
  const runtimeToken = parseRuntimeTokenResponse(await response.json());
270
1226
  this.cachedRuntimeToken = {
@@ -274,17 +1230,26 @@ export class AgentPayClient {
274
1230
  return runtimeToken.token;
275
1231
  }
276
1232
  async controlPlaneFetch(path, init, authorizationHeader) {
1233
+ // Every control-plane call carries the SDK version header so incompatible
1234
+ // client/server contract mismatches can fail fast.
277
1235
  return this.fetchImpl(`${this.controlPlaneBaseUrl}${path}`, {
278
1236
  ...init,
279
1237
  headers: {
280
1238
  ...(this.headers ?? {}),
281
1239
  ...(normalizeHeaders(init.headers) ?? {}),
282
1240
  Authorization: authorizationHeader,
1241
+ [sdkClientVersionHeaderName]: sdkClientVersion,
283
1242
  },
284
1243
  });
285
1244
  }
286
1245
  }
1246
+ /** Small factory wrapper for callers that prefer a function export. */
287
1247
  export function createAgentPayClient(options) {
288
1248
  return new AgentPayClient(options);
289
1249
  }
1250
+ export * from './agent-harness.js';
1251
+ export * from './contracts.js';
1252
+ export * from './harness-instructions.js';
1253
+ export { detectChallengeFromResponse } from './challenge-detection.js';
1254
+ export { sdkClientVersion, sdkClientVersionHeaderName } from './version.js';
290
1255
  //# sourceMappingURL=index.js.map