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