@402flow/sdk 0.1.0-alpha.13 → 0.1.0-alpha.15
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/README.md +319 -53
- package/dist/agent-harness.d.ts +161 -0
- package/dist/agent-harness.js +371 -0
- package/dist/agent-harness.js.map +1 -0
- package/dist/challenge-detection.d.ts +10 -0
- package/dist/challenge-detection.js +11 -0
- package/dist/challenge-detection.js.map +1 -1
- package/dist/contracts.d.ts +3690 -30
- package/dist/contracts.js +150 -0
- package/dist/contracts.js.map +1 -1
- package/dist/index.d.ts +63 -1
- package/dist/index.js +747 -11
- package/dist/index.js.map +1 -1
- package/dist/version.d.ts +2 -0
- package/dist/version.js +5 -0
- package/dist/version.js.map +1 -1
- package/package.json +8 -6
package/dist/index.js
CHANGED
|
@@ -1,7 +1,19 @@
|
|
|
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';
|
|
4
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
|
+
*/
|
|
5
17
|
export class FetchPaidError extends Error {
|
|
6
18
|
details;
|
|
7
19
|
kind;
|
|
@@ -33,6 +45,7 @@ export class FetchPaidError extends Error {
|
|
|
33
45
|
Object.setPrototypeOf(this, new.target.prototype);
|
|
34
46
|
}
|
|
35
47
|
}
|
|
48
|
+
/** Type guard for callers that catch unknown errors around fetchPaid flows. */
|
|
36
49
|
export function isFetchPaidError(error) {
|
|
37
50
|
return error instanceof FetchPaidError;
|
|
38
51
|
}
|
|
@@ -128,7 +141,30 @@ function tryParseJson(value) {
|
|
|
128
141
|
return undefined;
|
|
129
142
|
}
|
|
130
143
|
}
|
|
131
|
-
function
|
|
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) {
|
|
132
168
|
if (body === undefined || body === null) {
|
|
133
169
|
return undefined;
|
|
134
170
|
}
|
|
@@ -138,7 +174,30 @@ function getReplayableRequestBody(body) {
|
|
|
138
174
|
if (body instanceof URLSearchParams) {
|
|
139
175
|
return body.toString();
|
|
140
176
|
}
|
|
141
|
-
throw new Error(
|
|
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);
|
|
142
201
|
}
|
|
143
202
|
function hashRequestBody(body) {
|
|
144
203
|
if (!body) {
|
|
@@ -163,6 +222,604 @@ function parseRuntimeTokenResponse(payload) {
|
|
|
163
222
|
expiresAt,
|
|
164
223
|
};
|
|
165
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
|
+
/**
|
|
818
|
+
* Client bound to one organization/agent identity and one 402flow control plane.
|
|
819
|
+
*
|
|
820
|
+
* Most integrations either call fetchPaid() directly or use the explicit
|
|
821
|
+
* preparePaidRequest() -> executePreparedRequest() flow.
|
|
822
|
+
*/
|
|
166
823
|
export class AgentPayClient {
|
|
167
824
|
controlPlaneBaseUrl;
|
|
168
825
|
auth;
|
|
@@ -181,6 +838,82 @@ export class AgentPayClient {
|
|
|
181
838
|
this.fetchImpl = options.fetch ?? fetch;
|
|
182
839
|
this.headers = options.headers;
|
|
183
840
|
}
|
|
841
|
+
/**
|
|
842
|
+
* Probe or reuse a merchant challenge and return a normalized preparation
|
|
843
|
+
* result the caller can inspect before paying.
|
|
844
|
+
*/
|
|
845
|
+
async preparePaidRequest(input, init = {}, options = {}) {
|
|
846
|
+
const request = createPreparedHttpRequest(input, init);
|
|
847
|
+
if (options.challenge) {
|
|
848
|
+
const hints = await buildPreparedRequestHints(options, this.fetchImpl, options.challenge);
|
|
849
|
+
const validationIssues = buildPreparedValidationIssues(request, hints);
|
|
850
|
+
return {
|
|
851
|
+
kind: 'ready',
|
|
852
|
+
protocol: options.challenge.protocol,
|
|
853
|
+
request,
|
|
854
|
+
challenge: paidRequestChallengeSchema.parse(options.challenge),
|
|
855
|
+
...(buildPreparedPaymentRequirement(options.challenge)
|
|
856
|
+
? {
|
|
857
|
+
paymentRequirement: buildPreparedPaymentRequirement(options.challenge),
|
|
858
|
+
}
|
|
859
|
+
: {}),
|
|
860
|
+
hints,
|
|
861
|
+
validationIssues,
|
|
862
|
+
nextAction: derivePreparedNextAction('ready', validationIssues),
|
|
863
|
+
};
|
|
864
|
+
}
|
|
865
|
+
const initialResponse = await this.fetchImpl(input, init);
|
|
866
|
+
const challenge = await detectChallengeFromResponse(initialResponse);
|
|
867
|
+
const probe = {
|
|
868
|
+
responseStatus: initialResponse.status,
|
|
869
|
+
confirmedAt: new Date().toISOString(),
|
|
870
|
+
};
|
|
871
|
+
const hints = await buildPreparedRequestHints(options, this.fetchImpl, challenge);
|
|
872
|
+
const validationIssues = buildPreparedValidationIssues(request, hints);
|
|
873
|
+
if (!challenge) {
|
|
874
|
+
return {
|
|
875
|
+
kind: 'passthrough',
|
|
876
|
+
protocol: 'none',
|
|
877
|
+
request,
|
|
878
|
+
hints,
|
|
879
|
+
probe,
|
|
880
|
+
validationIssues,
|
|
881
|
+
nextAction: derivePreparedNextAction('passthrough', validationIssues),
|
|
882
|
+
};
|
|
883
|
+
}
|
|
884
|
+
return {
|
|
885
|
+
kind: 'ready',
|
|
886
|
+
protocol: challenge.protocol,
|
|
887
|
+
request,
|
|
888
|
+
challenge: paidRequestChallengeSchema.parse(challenge),
|
|
889
|
+
...(buildPreparedPaymentRequirement(challenge)
|
|
890
|
+
? { paymentRequirement: buildPreparedPaymentRequirement(challenge) }
|
|
891
|
+
: {}),
|
|
892
|
+
hints,
|
|
893
|
+
probe,
|
|
894
|
+
validationIssues,
|
|
895
|
+
nextAction: derivePreparedNextAction('ready', validationIssues),
|
|
896
|
+
};
|
|
897
|
+
}
|
|
898
|
+
/**
|
|
899
|
+
* Execute the exact request that was previously prepared, without re-probing
|
|
900
|
+
* the merchant first.
|
|
901
|
+
*/
|
|
902
|
+
async executePreparedRequest(prepared, request = {}) {
|
|
903
|
+
return this.fetchPaid(prepared.request.url, {
|
|
904
|
+
method: prepared.request.method,
|
|
905
|
+
...(prepared.request.headers ? { headers: prepared.request.headers } : {}),
|
|
906
|
+
...(prepared.request.body !== undefined ? { body: prepared.request.body } : {}),
|
|
907
|
+
}, {
|
|
908
|
+
...request,
|
|
909
|
+
challenge: prepared.challenge,
|
|
910
|
+
});
|
|
911
|
+
}
|
|
912
|
+
/**
|
|
913
|
+
* Fast-path helper that probes when needed, asks the control plane for a paid
|
|
914
|
+
* execution decision, and returns either passthrough or success. All non-success
|
|
915
|
+
* paid outcomes are thrown as FetchPaidError.
|
|
916
|
+
*/
|
|
184
917
|
async fetchPaid(input, init = {}, request) {
|
|
185
918
|
let challenge = request.challenge;
|
|
186
919
|
if (!challenge) {
|
|
@@ -198,6 +931,7 @@ export class AgentPayClient {
|
|
|
198
931
|
const decision = await this.requestPaymentDecision(decisionRequest, challenge.protocol);
|
|
199
932
|
return this.mapDecisionToPaidResponse(decision, challenge.protocol);
|
|
200
933
|
}
|
|
934
|
+
/** Lookup a durable receipt by id through the control plane. */
|
|
201
935
|
async lookupReceipt(receiptId) {
|
|
202
936
|
const response = await this.controlPlaneFetch(`/api/sdk/receipts/${receiptId}`, {
|
|
203
937
|
method: 'GET',
|
|
@@ -209,20 +943,13 @@ export class AgentPayClient {
|
|
|
209
943
|
return sdkReceiptResponseSchema.parse(await response.json());
|
|
210
944
|
}
|
|
211
945
|
createDecisionRequest(input, init, request, challenge) {
|
|
212
|
-
const requestBody = getReplayableRequestBody(init.body);
|
|
213
946
|
const { challenge: _challenge, idempotencyKey, ...requestContext } = request;
|
|
214
947
|
return sdkPaymentDecisionRequestSchema.parse({
|
|
215
948
|
context: {
|
|
216
949
|
...this.identity,
|
|
217
950
|
...requestContext,
|
|
218
951
|
},
|
|
219
|
-
request:
|
|
220
|
-
url: input,
|
|
221
|
-
method: (init.method ?? 'GET').toUpperCase(),
|
|
222
|
-
headers: normalizeHeaders(init.headers),
|
|
223
|
-
body: requestBody,
|
|
224
|
-
bodyHash: hashRequestBody(requestBody),
|
|
225
|
-
},
|
|
952
|
+
request: createPreparedHttpRequest(input, init),
|
|
226
953
|
challenge: {
|
|
227
954
|
protocol: challenge.protocol,
|
|
228
955
|
headers: challenge.headers,
|
|
@@ -232,6 +959,9 @@ export class AgentPayClient {
|
|
|
232
959
|
});
|
|
233
960
|
}
|
|
234
961
|
async requestPaymentDecision(decisionRequest, protocol) {
|
|
962
|
+
// The control plane is the single source of truth for payment policy and
|
|
963
|
+
// receipt persistence. Any response that does not match the SDK contract is
|
|
964
|
+
// downgraded to request_failed.
|
|
235
965
|
const decisionResponse = await this.controlPlaneFetch('/api/sdk/payment-decisions', {
|
|
236
966
|
method: 'POST',
|
|
237
967
|
headers: {
|
|
@@ -277,6 +1007,8 @@ export class AgentPayClient {
|
|
|
277
1007
|
});
|
|
278
1008
|
}
|
|
279
1009
|
mapDecisionToPaidResponse(decision, protocol) {
|
|
1010
|
+
// Only allow returns normally. Every other durable or non-durable paid outcome
|
|
1011
|
+
// is surfaced as a typed FetchPaidError so caller control flow stays explicit.
|
|
280
1012
|
switch (decision.outcome) {
|
|
281
1013
|
case 'allow': {
|
|
282
1014
|
const response = {
|
|
@@ -413,6 +1145,8 @@ export class AgentPayClient {
|
|
|
413
1145
|
return runtimeToken.token;
|
|
414
1146
|
}
|
|
415
1147
|
async controlPlaneFetch(path, init, authorizationHeader) {
|
|
1148
|
+
// Every control-plane call carries the SDK version header so incompatible
|
|
1149
|
+
// client/server contract mismatches can fail fast.
|
|
416
1150
|
return this.fetchImpl(`${this.controlPlaneBaseUrl}${path}`, {
|
|
417
1151
|
...init,
|
|
418
1152
|
headers: {
|
|
@@ -424,9 +1158,11 @@ export class AgentPayClient {
|
|
|
424
1158
|
});
|
|
425
1159
|
}
|
|
426
1160
|
}
|
|
1161
|
+
/** Small factory wrapper for callers that prefer a function export. */
|
|
427
1162
|
export function createAgentPayClient(options) {
|
|
428
1163
|
return new AgentPayClient(options);
|
|
429
1164
|
}
|
|
1165
|
+
export * from './agent-harness.js';
|
|
430
1166
|
export * from './contracts.js';
|
|
431
1167
|
export { detectChallengeFromResponse } from './challenge-detection.js';
|
|
432
1168
|
export { sdkClientVersion, sdkClientVersionHeaderName } from './version.js';
|