@devlab.group/agent-commerce 0.1.0-alpha.0 → 1.0.0
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 +144 -46
- package/dist/{chunk-CZ6ZDAYD.js → chunk-BQNE3A5H.js} +9 -7
- package/dist/chunk-BQNE3A5H.js.map +1 -0
- package/dist/chunk-MEPETSP6.js +255 -0
- package/dist/chunk-MEPETSP6.js.map +1 -0
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.js +401 -61
- package/dist/cli/index.js.map +1 -1
- package/dist/guardrails-Dmwi63KB.d.ts +38 -0
- package/dist/index.d.ts +38 -19
- package/dist/index.js +177 -48
- package/dist/index.js.map +1 -1
- package/dist/mcp.d.ts +2 -2
- package/dist/mcp.js +2 -2
- package/dist/mcp.js.map +1 -1
- package/dist/{protocol-adapter-C0goMgLK.d.ts → protocol-adapter-CZPOb9uG.d.ts} +2 -3
- package/dist/{runtime-LCk2TzGN.d.ts → runtime-B-9IjMbO.d.ts} +17 -5
- package/dist/x402.d.ts +83 -17
- package/dist/x402.js +321 -216
- package/dist/x402.js.map +1 -1
- package/package.json +19 -7
- package/dist/chunk-CZ6ZDAYD.js.map +0 -1
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How the gateway authenticates to a remote facilitator.
|
|
3
|
+
*
|
|
4
|
+
* Deliberately a list, not a vendor: x402 facilitators are not a Coinbase-only
|
|
5
|
+
* category, and a scheme that only fitted one vendor's credentials would make
|
|
6
|
+
* the abstraction a fiction.
|
|
7
|
+
*
|
|
8
|
+
* - `none` — the facilitator takes no credential (the public testnet one).
|
|
9
|
+
* - `bearer` — a static token. Covers most self-hosted and third-party
|
|
10
|
+
* facilitators, and needs nothing installed.
|
|
11
|
+
* - `cdp` — Coinbase Developer Platform, which signs a fresh JWT per request
|
|
12
|
+
* over method + host + path, so a static header cannot express it. Handled by
|
|
13
|
+
* the optional peer `@coinbase/x402`, imported only when this type is
|
|
14
|
+
* configured. Anyone not using CDP never installs it.
|
|
15
|
+
*
|
|
16
|
+
* A facilitator whose scheme is none of these is refused at config load rather
|
|
17
|
+
* than sent nothing.
|
|
18
|
+
*/
|
|
19
|
+
type FacilitatorAuth = {
|
|
20
|
+
readonly type: 'none';
|
|
21
|
+
} | {
|
|
22
|
+
readonly type: 'bearer';
|
|
23
|
+
readonly token: string;
|
|
24
|
+
} | {
|
|
25
|
+
readonly type: 'cdp';
|
|
26
|
+
readonly apiKeyId: string;
|
|
27
|
+
readonly apiKeySecret: string;
|
|
28
|
+
};
|
|
29
|
+
type X402FacilitatorConfig = {
|
|
30
|
+
readonly mode: 'local';
|
|
31
|
+
readonly signerPrivateKey: string;
|
|
32
|
+
} | {
|
|
33
|
+
readonly mode: 'remote';
|
|
34
|
+
readonly url: string;
|
|
35
|
+
readonly auth: FacilitatorAuth;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export type { FacilitatorAuth as F, X402FacilitatorConfig as X };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { I as IsoTimestamp, D as DecimalAmount, P as PaymentMethodName, B as BackendHandler, A as AdapterDescriptor, a as AdapterHealth, C as CommerceResource, R as ResourceRegistry, b as PaymentProvider, L as Logger, c as Clock, d as IdGenerator } from './runtime-
|
|
2
|
-
export { e as BackendMethod, f as DEFAULT_BACKEND_TIMEOUT_MS, J as JsonSchema, N as NOOP_LOGGER, g as PaymentChallenge, h as PaymentContext, i as PaymentRequirement, j as PaymentResult, k as PaymentSettlementContext, l as PaymentSubmission, m as PaymentVerificationContext, n as Pricing, o as ProtocolName, s as systemClock } from './runtime-
|
|
3
|
-
import {
|
|
4
|
-
|
|
1
|
+
import { I as IsoTimestamp, D as DecimalAmount, P as PaymentMethodName, B as BackendHandler, A as AdapterDescriptor, a as AdapterHealth, C as CommerceResource, R as ResourceRegistry, b as PaymentProvider, L as Logger, c as Clock, d as IdGenerator } from './runtime-B-9IjMbO.js';
|
|
2
|
+
export { e as BackendMethod, f as DEFAULT_BACKEND_TIMEOUT_MS, J as JsonSchema, N as NOOP_LOGGER, g as PaymentChallenge, h as PaymentContext, i as PaymentRequirement, j as PaymentResult, k as PaymentSettlementContext, l as PaymentSubmission, m as PaymentVerificationContext, n as Pricing, o as ProtocolName, s as systemClock } from './runtime-B-9IjMbO.js';
|
|
3
|
+
import { X as X402FacilitatorConfig } from './guardrails-Dmwi63KB.js';
|
|
4
|
+
import { D as DeliveredOutcome, P as PaymentRequiredOutcome, C as CommerceEvent, a as PaymentAttempt, b as CommerceReceipt, E as ExecutionPipeline, c as ProtocolAdapter } from './protocol-adapter-CZPOb9uG.js';
|
|
5
|
+
export { d as COMMERCE_EVENT_TYPES, e as CanonicalRequest, f as CommerceEventType, g as EventSink, h as ExecutionOutcome, H as HttpProtocolAdapter, i as ProtocolAdapterContext, j as isHttpProtocolAdapter } from './protocol-adapter-CZPOb9uG.js';
|
|
5
6
|
import { FastifyInstance } from 'fastify';
|
|
6
7
|
import 'node:http';
|
|
7
8
|
|
|
@@ -93,13 +94,29 @@ declare function toCommerceError(value: unknown, fallbackCode?: CommerceErrorCod
|
|
|
93
94
|
|
|
94
95
|
/**
|
|
95
96
|
* Reserved input property carrying a payment proof on protocols that have no
|
|
96
|
-
* header channel (MCP tool calls). Over HTTP the `
|
|
97
|
+
* header channel (MCP tool calls). Over HTTP the `PAYMENT-SIGNATURE` header is
|
|
98
|
+
* used.
|
|
97
99
|
*/
|
|
98
100
|
declare const PAYMENT_INPUT_FIELD = "_payment";
|
|
99
|
-
/**
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
101
|
+
/**
|
|
102
|
+
* HTTP request header carrying the payment proof.
|
|
103
|
+
*
|
|
104
|
+
* x402 v2 header names, lowercased because that is how Node presents incoming
|
|
105
|
+
* headers. v1's `X-PAYMENT` / `X-PAYMENT-RESPONSE` pair is not accepted: this
|
|
106
|
+
* gateway speaks one protocol version, and quietly honouring both would mean
|
|
107
|
+
* two verification paths for the same money.
|
|
108
|
+
*/
|
|
109
|
+
declare const PAYMENT_HEADER = "payment-signature";
|
|
110
|
+
/** HTTP response header carrying the settlement result. */
|
|
111
|
+
declare const PAYMENT_RESPONSE_HEADER = "payment-response";
|
|
112
|
+
/**
|
|
113
|
+
* HTTP response header carrying the base64 payment challenge on a 402.
|
|
114
|
+
*
|
|
115
|
+
* The 402 body still carries {@link PaymentRequiredEnvelope} — richer, and the
|
|
116
|
+
* only channel MCP has — but an x402 v2 client reads the challenge from this
|
|
117
|
+
* header and ignores the body, so both are sent.
|
|
118
|
+
*/
|
|
119
|
+
declare const PAYMENT_REQUIRED_HEADER = "payment-required";
|
|
103
120
|
/**
|
|
104
121
|
* Key under which a protocol adapter attaches a {@link DeliverySummary} to its
|
|
105
122
|
* result metadata — MCP's `CallToolResult._meta`, and anywhere else a protocol
|
|
@@ -130,6 +147,12 @@ interface PaymentRequiredEnvelope {
|
|
|
130
147
|
readonly expiresAt?: IsoTimestamp;
|
|
131
148
|
/** Provider-native requirement objects, passed through verbatim. */
|
|
132
149
|
readonly accepts: readonly Readonly<Record<string, unknown>>[];
|
|
150
|
+
/**
|
|
151
|
+
* The provider's own challenge document, verbatim — see
|
|
152
|
+
* {@link PaymentChallenge.envelope}. Present for providers that have one;
|
|
153
|
+
* a buyer's protocol client can hand this straight to its SDK.
|
|
154
|
+
*/
|
|
155
|
+
readonly envelope?: Readonly<Record<string, unknown>>;
|
|
133
156
|
};
|
|
134
157
|
}
|
|
135
158
|
interface ErrorEnvelope {
|
|
@@ -150,7 +173,7 @@ interface ErrorEnvelope {
|
|
|
150
173
|
* `/api/receipts` is an operator route behind `server.adminToken`.
|
|
151
174
|
*
|
|
152
175
|
* Before this existed the two surfaces disagreed: HTTP callers received a
|
|
153
|
-
* settlement summary in
|
|
176
|
+
* settlement summary in the payment-response header while MCP callers received
|
|
154
177
|
* nothing, so an MCP buyer's only route to their own receipt was the merchant's
|
|
155
178
|
* ledger. Both surfaces now emit this same shape.
|
|
156
179
|
*/
|
|
@@ -263,7 +286,7 @@ interface ReceiptStore {
|
|
|
263
286
|
* Receipts whose delivery did not succeed — `backendStatus` outside 2xx.
|
|
264
287
|
*
|
|
265
288
|
* A paid-but-undelivered purchase is the one row a merchant must notice:
|
|
266
|
-
* settlement is final and
|
|
289
|
+
* settlement is final and this release has no refunds, so *seeing* it is the only
|
|
267
290
|
* remedy available. Counted rather than listed for the same reason as
|
|
268
291
|
* {@link countReceipts} — `listReceipts` is clamped, so counting by list
|
|
269
292
|
* length silently saturates.
|
|
@@ -353,13 +376,9 @@ interface GatewayConfig {
|
|
|
353
376
|
readonly assetDecimals: number;
|
|
354
377
|
readonly payTo: string;
|
|
355
378
|
readonly maxTimeoutSeconds: number;
|
|
356
|
-
readonly facilitator:
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
} | {
|
|
360
|
-
readonly mode: 'remote';
|
|
361
|
-
readonly url: string;
|
|
362
|
-
};
|
|
379
|
+
readonly facilitator: X402FacilitatorConfig;
|
|
380
|
+
readonly allowMainnet?: boolean;
|
|
381
|
+
readonly allowUnauthenticatedFacilitator?: boolean;
|
|
363
382
|
};
|
|
364
383
|
};
|
|
365
384
|
}
|
|
@@ -409,4 +428,4 @@ interface SqliteReceiptStoreOptions {
|
|
|
409
428
|
}
|
|
410
429
|
declare function createSqliteReceiptStore(options: SqliteReceiptStoreOptions): ReceiptStore;
|
|
411
430
|
|
|
412
|
-
export { AdapterDescriptor, AdapterHealth, type BackendExecutor, BackendHandler, type BackendRequest, type BackendResponse, COMMERCE_ERROR_CODES, COMMERCE_ERROR_HTTP_STATUS, Clock, CommerceError, type CommerceErrorCode, type CommerceErrorInfo, type CommerceErrorOptions, CommerceEvent, CommerceReceipt, CommerceResource, DELIVERY_SUMMARY_META_KEY, DecimalAmount, DeliveredOutcome, type DeliverySummary, type ErrorEnvelope, ExecutionPipeline, type GatewayConfig, type GatewayInstance, type GatewayOptions, IdGenerator, IsoTimestamp, type ListOptions, Logger, PAYMENT_HEADER, PAYMENT_INPUT_FIELD, PAYMENT_RESPONSE_HEADER, PaymentAttempt, type PaymentAttemptReservation, type PaymentAttemptUpdate, PaymentMethodName, PaymentProvider, type PaymentRequiredEnvelope, PaymentRequiredOutcome, ProtocolAdapter, RETRYABLE_ERROR_CODES, type ReceiptStore, ResourceRegistry, createGateway, createSqliteReceiptStore, isCommerceError, isPaymentRequiredEnvelope, loadConfig, parseConfig, createSqliteReceiptStore as receipts, toCommerceError, toDeliverySummary, toErrorEnvelope, toPaymentRequiredEnvelope };
|
|
431
|
+
export { AdapterDescriptor, AdapterHealth, type BackendExecutor, BackendHandler, type BackendRequest, type BackendResponse, COMMERCE_ERROR_CODES, COMMERCE_ERROR_HTTP_STATUS, Clock, CommerceError, type CommerceErrorCode, type CommerceErrorInfo, type CommerceErrorOptions, CommerceEvent, CommerceReceipt, CommerceResource, DELIVERY_SUMMARY_META_KEY, DecimalAmount, DeliveredOutcome, type DeliverySummary, type ErrorEnvelope, ExecutionPipeline, type GatewayConfig, type GatewayInstance, type GatewayOptions, IdGenerator, IsoTimestamp, type ListOptions, Logger, PAYMENT_HEADER, PAYMENT_INPUT_FIELD, PAYMENT_REQUIRED_HEADER, PAYMENT_RESPONSE_HEADER, PaymentAttempt, type PaymentAttemptReservation, type PaymentAttemptUpdate, PaymentMethodName, PaymentProvider, type PaymentRequiredEnvelope, PaymentRequiredOutcome, ProtocolAdapter, RETRYABLE_ERROR_CODES, type ReceiptStore, ResourceRegistry, createGateway, createSqliteReceiptStore, isCommerceError, isPaymentRequiredEnvelope, loadConfig, parseConfig, createSqliteReceiptStore as receipts, toCommerceError, toDeliverySummary, toErrorEnvelope, toPaymentRequiredEnvelope };
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import { resolveX402Deployment, resolveDeploymentMode, requireNetworkProfile } from './chunk-MEPETSP6.js';
|
|
2
|
+
import { CommerceError, PAYMENT_INPUT_FIELD, systemClock, NOOP_LOGGER, PACKAGE_VERSION, toCommerceError, DEFAULT_BACKEND_TIMEOUT_MS, toErrorEnvelope, isHttpProtocolAdapter, isCommerceError, PAYMENT_HEADER, toPaymentRequiredEnvelope, PAYMENT_REQUIRED_HEADER, PAYMENT_RESPONSE_HEADER } from './chunk-BQNE3A5H.js';
|
|
3
|
+
export { COMMERCE_ERROR_CODES, COMMERCE_ERROR_HTTP_STATUS, COMMERCE_EVENT_TYPES, CommerceError, DEFAULT_BACKEND_TIMEOUT_MS, DELIVERY_SUMMARY_META_KEY, NOOP_LOGGER, PAYMENT_HEADER, PAYMENT_INPUT_FIELD, PAYMENT_REQUIRED_HEADER, PAYMENT_RESPONSE_HEADER, RETRYABLE_ERROR_CODES, isCommerceError, isHttpProtocolAdapter, isPaymentRequiredEnvelope, systemClock, toCommerceError, toDeliverySummary, toErrorEnvelope, toPaymentRequiredEnvelope } from './chunk-BQNE3A5H.js';
|
|
4
|
+
import { promises, existsSync, mkdirSync, accessSync, constants, closeSync, openSync, chmodSync } from 'fs';
|
|
4
5
|
import * as path from 'path';
|
|
5
6
|
import { dirname } from 'path';
|
|
6
7
|
import { parse, YAMLError } from 'yaml';
|
|
@@ -13,6 +14,9 @@ import Database from 'better-sqlite3';
|
|
|
13
14
|
|
|
14
15
|
// src/config/env.ts
|
|
15
16
|
var PLACEHOLDER_PATTERN = /\$\{([A-Za-z_][A-Za-z0-9_]*)(:-([^}]*))?\}/g;
|
|
17
|
+
var BRACE_TOKEN = /\$\{[^{}]*\}/g;
|
|
18
|
+
var SUPPORTED_TOKEN = /^\$\{[A-Za-z_][A-Za-z0-9_]*(:-[^}]*)?\}$/;
|
|
19
|
+
var NESTED_PLACEHOLDER = /\$\{[^{}]*\$\{/;
|
|
16
20
|
function substituteEnv(value, env, path2 = "$") {
|
|
17
21
|
if (typeof value === "string") {
|
|
18
22
|
return substituteString(value, env, path2);
|
|
@@ -30,19 +34,26 @@ function substituteEnv(value, env, path2 = "$") {
|
|
|
30
34
|
return value;
|
|
31
35
|
}
|
|
32
36
|
function substituteString(value, env, path2) {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
37
|
+
assertTemplateIsSupported(value, path2);
|
|
38
|
+
return substituteOnce(value, env, path2);
|
|
39
|
+
}
|
|
40
|
+
function assertTemplateIsSupported(value, path2) {
|
|
41
|
+
if (NESTED_PLACEHOLDER.test(value)) {
|
|
42
|
+
throw new CommerceError(
|
|
43
|
+
"CONFIG_INVALID",
|
|
44
|
+
`Configuration value at "${path2}" nests placeholders (e.g. "\${A:-\${B}}"), which is not supported \u2014 the inner one is not resolved and its closing brace ends up in the value. Use a plain default, or set the variable.`,
|
|
45
|
+
{ details: { path: path2 } }
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
BRACE_TOKEN.lastIndex = 0;
|
|
49
|
+
for (const [token] of value.matchAll(BRACE_TOKEN)) {
|
|
50
|
+
if (SUPPORTED_TOKEN.test(token)) continue;
|
|
39
51
|
throw new CommerceError(
|
|
40
52
|
"CONFIG_INVALID",
|
|
41
|
-
`Configuration value at "${path2}"
|
|
42
|
-
{ details: { path: path2,
|
|
53
|
+
`Configuration value at "${path2}" contains "${token}", which is not a supported placeholder. Only "\${VAR}" and "\${VAR:-default}" are recognised; shell forms such as "\${VAR-default}", "\${VAR:=default}" and "\${VAR:?message}" would load as literal text.`,
|
|
54
|
+
{ details: { path: path2, token } }
|
|
43
55
|
);
|
|
44
56
|
}
|
|
45
|
-
return substituted;
|
|
46
57
|
}
|
|
47
58
|
function substituteOnce(value, env, path2) {
|
|
48
59
|
return value.replace(
|
|
@@ -952,6 +963,7 @@ function createExecutionPipeline(options) {
|
|
|
952
963
|
provider: paymentResult.provider,
|
|
953
964
|
amount: paymentResult.amount,
|
|
954
965
|
currency: paymentResult.currency,
|
|
966
|
+
...paymentResult.network !== void 0 ? { network: paymentResult.network } : {},
|
|
955
967
|
...paymentResult.externalReference !== void 0 ? { externalReference: paymentResult.externalReference } : {}
|
|
956
968
|
}
|
|
957
969
|
}
|
|
@@ -1109,11 +1121,36 @@ var StorageSchema = z.object({
|
|
|
1109
1121
|
path: z.string().min(1)
|
|
1110
1122
|
}).strict()
|
|
1111
1123
|
}).strict();
|
|
1124
|
+
var RESERVED_GATEWAY_PATHS = [
|
|
1125
|
+
"/health",
|
|
1126
|
+
"/ready",
|
|
1127
|
+
"/.well-known/agent-commerce",
|
|
1128
|
+
"/api/resources",
|
|
1129
|
+
"/api/resources/:id/invoke",
|
|
1130
|
+
"/api/receipts",
|
|
1131
|
+
"/api/events",
|
|
1132
|
+
"/api/events/stream"
|
|
1133
|
+
];
|
|
1134
|
+
var McpMountPathSchema = z.string().min(1).refine((value) => value.startsWith("/"), {
|
|
1135
|
+
message: 'must start with "/" \u2014 it is an absolute gateway path, e.g. "/mcp".'
|
|
1136
|
+
}).refine((value) => !/[:*?\s]/.test(value), {
|
|
1137
|
+
message: 'must not contain ":", "*", "?" or whitespace \u2014 the mount is a literal path prefix, not a Fastify route pattern, and registers its own wildcard.'
|
|
1138
|
+
}).refine(
|
|
1139
|
+
(value) => {
|
|
1140
|
+
const base = value.replace(/\/+$/, "");
|
|
1141
|
+
return !RESERVED_GATEWAY_PATHS.some(
|
|
1142
|
+
(reserved) => reserved === base || reserved.startsWith(`${base}/`)
|
|
1143
|
+
);
|
|
1144
|
+
},
|
|
1145
|
+
{
|
|
1146
|
+
message: `must not collide with a route the gateway already serves (${RESERVED_GATEWAY_PATHS.join(", ")}); pick a dedicated prefix such as "/mcp".`
|
|
1147
|
+
}
|
|
1148
|
+
);
|
|
1112
1149
|
var ProtocolsSchema = z.object({
|
|
1113
1150
|
http: z.object({ enabled: BooleanOrString }).strict(),
|
|
1114
1151
|
mcp: z.object({
|
|
1115
1152
|
enabled: BooleanOrString,
|
|
1116
|
-
mountPath:
|
|
1153
|
+
mountPath: McpMountPathSchema
|
|
1117
1154
|
}).strict()
|
|
1118
1155
|
}).strict();
|
|
1119
1156
|
var BackendMethodSchema = z.enum(["GET", "POST", "PUT", "PATCH", "DELETE"]);
|
|
@@ -1148,6 +1185,17 @@ var ResourceEntrySchema = z.object({
|
|
|
1148
1185
|
payments: z.array(z.string().min(1)).optional()
|
|
1149
1186
|
}).strict();
|
|
1150
1187
|
var ResourcesMapSchema = z.record(z.string().min(1), ResourceEntrySchema);
|
|
1188
|
+
var FacilitatorAuthSchema = z.discriminatedUnion("type", [
|
|
1189
|
+
z.object({ type: z.literal("none") }).strict(),
|
|
1190
|
+
z.object({ type: z.literal("bearer"), token: z.string().min(1) }).strict(),
|
|
1191
|
+
// Coinbase Developer Platform. Needs the optional peer `@coinbase/x402`,
|
|
1192
|
+
// which signs a fresh JWT per request; a static header cannot express it.
|
|
1193
|
+
z.object({
|
|
1194
|
+
type: z.literal("cdp"),
|
|
1195
|
+
apiKeyId: z.string().min(1),
|
|
1196
|
+
apiKeySecret: z.string().min(1)
|
|
1197
|
+
}).strict()
|
|
1198
|
+
]);
|
|
1151
1199
|
var FacilitatorSchema = z.discriminatedUnion("mode", [
|
|
1152
1200
|
z.object({
|
|
1153
1201
|
mode: z.literal("local"),
|
|
@@ -1155,7 +1203,12 @@ var FacilitatorSchema = z.discriminatedUnion("mode", [
|
|
|
1155
1203
|
}).strict(),
|
|
1156
1204
|
z.object({
|
|
1157
1205
|
mode: z.literal("remote"),
|
|
1158
|
-
url: z.string().min(1)
|
|
1206
|
+
url: z.string().min(1),
|
|
1207
|
+
// Absent means "this facilitator takes no credential" — an explicit
|
|
1208
|
+
// statement, normalised to `{ type: 'none' }` below. On a mainnet that
|
|
1209
|
+
// combination is refused outright, so the default can never quietly
|
|
1210
|
+
// become an unauthenticated production facilitator.
|
|
1211
|
+
auth: FacilitatorAuthSchema.optional()
|
|
1159
1212
|
}).strict()
|
|
1160
1213
|
]);
|
|
1161
1214
|
var X402Schema = z.object({
|
|
@@ -1168,7 +1221,11 @@ var X402Schema = z.object({
|
|
|
1168
1221
|
assetDecimals: NumberOrString,
|
|
1169
1222
|
payTo: z.string().min(1),
|
|
1170
1223
|
maxTimeoutSeconds: NumberOrString,
|
|
1171
|
-
facilitator: FacilitatorSchema
|
|
1224
|
+
facilitator: FacilitatorSchema,
|
|
1225
|
+
/** Real funds. Never defaulted — see src/payments/x402/guardrails.ts. */
|
|
1226
|
+
allowMainnet: BooleanOrString.optional(),
|
|
1227
|
+
/** Accepts a mainnet facilitator that takes no credential. Never defaulted. */
|
|
1228
|
+
allowUnauthenticatedFacilitator: BooleanOrString.optional()
|
|
1172
1229
|
}).strict();
|
|
1173
1230
|
var PaymentsSchema = z.object({
|
|
1174
1231
|
x402: X402Schema.optional()
|
|
@@ -1290,14 +1347,12 @@ function normalise(raw) {
|
|
|
1290
1347
|
}
|
|
1291
1348
|
};
|
|
1292
1349
|
const x402Raw = raw.payments.x402;
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
}
|
|
1300
|
-
const x402 = x402Raw ? {
|
|
1350
|
+
const facilitator = x402Raw === void 0 ? void 0 : x402Raw.facilitator.mode === "local" ? { mode: "local", signerPrivateKey: x402Raw.facilitator.signerPrivateKey } : {
|
|
1351
|
+
mode: "remote",
|
|
1352
|
+
url: x402Raw.facilitator.url,
|
|
1353
|
+
auth: x402Raw.facilitator.auth ?? { type: "none" }
|
|
1354
|
+
};
|
|
1355
|
+
const x402 = x402Raw && facilitator ? {
|
|
1301
1356
|
enabled: toBoolean(x402Raw.enabled, "payments.x402.enabled"),
|
|
1302
1357
|
network: x402Raw.network,
|
|
1303
1358
|
rpcUrl: x402Raw.rpcUrl,
|
|
@@ -1309,18 +1364,35 @@ function normalise(raw) {
|
|
|
1309
1364
|
max: 36
|
|
1310
1365
|
}),
|
|
1311
1366
|
payTo: x402Raw.payTo,
|
|
1312
|
-
maxTimeoutSeconds: toNumber(
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1367
|
+
maxTimeoutSeconds: toNumber(
|
|
1368
|
+
x402Raw.maxTimeoutSeconds,
|
|
1369
|
+
"payments.x402.maxTimeoutSeconds",
|
|
1370
|
+
{
|
|
1371
|
+
min: 1
|
|
1372
|
+
}
|
|
1373
|
+
),
|
|
1374
|
+
facilitator,
|
|
1375
|
+
...x402Raw.allowMainnet !== void 0 ? { allowMainnet: toBoolean(x402Raw.allowMainnet, "payments.x402.allowMainnet") } : {},
|
|
1376
|
+
...x402Raw.allowUnauthenticatedFacilitator !== void 0 ? {
|
|
1377
|
+
allowUnauthenticatedFacilitator: toBoolean(
|
|
1378
|
+
x402Raw.allowUnauthenticatedFacilitator,
|
|
1379
|
+
"payments.x402.allowUnauthenticatedFacilitator"
|
|
1380
|
+
)
|
|
1381
|
+
} : {}
|
|
1320
1382
|
} : void 0;
|
|
1321
1383
|
if (x402) {
|
|
1322
1384
|
validateAddress("payments.x402.payTo", x402.payTo);
|
|
1323
1385
|
validateAddress("payments.x402.asset", x402.asset);
|
|
1386
|
+
resolveX402Deployment({
|
|
1387
|
+
network: x402.network,
|
|
1388
|
+
payTo: x402.payTo,
|
|
1389
|
+
asset: x402.asset,
|
|
1390
|
+
assetName: x402.assetName,
|
|
1391
|
+
assetVersion: x402.assetVersion,
|
|
1392
|
+
facilitator: x402.facilitator,
|
|
1393
|
+
...x402.allowMainnet !== void 0 ? { allowMainnet: x402.allowMainnet } : {},
|
|
1394
|
+
...x402.allowUnauthenticatedFacilitator !== void 0 ? { allowUnauthenticatedFacilitator: x402.allowUnauthenticatedFacilitator } : {}
|
|
1395
|
+
});
|
|
1324
1396
|
}
|
|
1325
1397
|
const resources = Object.entries(raw.resources).map(
|
|
1326
1398
|
([id, entry]) => normaliseResource(id, entry, protocols, x402)
|
|
@@ -1365,7 +1437,7 @@ function normaliseResource(id, entry, protocols, x402) {
|
|
|
1365
1437
|
}
|
|
1366
1438
|
for (const protocol of entry.expose) {
|
|
1367
1439
|
if (!SUPPORTED_PROTOCOLS.has(protocol)) {
|
|
1368
|
-
const hint = protocol === "ucp" ? " (UCP is planned, not supported in
|
|
1440
|
+
const hint = protocol === "ucp" ? " (UCP is planned, not supported in this release)" : "";
|
|
1369
1441
|
throw new CommerceError(
|
|
1370
1442
|
"CONFIG_INVALID",
|
|
1371
1443
|
`Resource "${id}" exposes unsupported protocol "${protocol}"${hint}. Supported: http, mcp.`,
|
|
@@ -1401,7 +1473,7 @@ function normaliseResource(id, entry, protocols, x402) {
|
|
|
1401
1473
|
if (entry.pricing.type === "dynamic") {
|
|
1402
1474
|
throw new CommerceError(
|
|
1403
1475
|
"CONFIG_INVALID",
|
|
1404
|
-
`Resource "${id}" uses pricing.type "dynamic", which is not supported in
|
|
1476
|
+
`Resource "${id}" uses pricing.type "dynamic", which is not supported in this release`,
|
|
1405
1477
|
{ details: { path: `resources.${id}.pricing.type`, resourceId: id } }
|
|
1406
1478
|
);
|
|
1407
1479
|
}
|
|
@@ -1478,6 +1550,10 @@ function defaultClosedObjectSchema(schema) {
|
|
|
1478
1550
|
if (isPlainObject4(items)) {
|
|
1479
1551
|
result["items"] = defaultClosedObjectSchema(items);
|
|
1480
1552
|
}
|
|
1553
|
+
const additional = schema["additionalProperties"];
|
|
1554
|
+
if (isPlainObject4(additional)) {
|
|
1555
|
+
result["additionalProperties"] = defaultClosedObjectSchema(additional);
|
|
1556
|
+
}
|
|
1481
1557
|
return result;
|
|
1482
1558
|
}
|
|
1483
1559
|
var EMPTY_CLOSED_OBJECT_SCHEMA = {
|
|
@@ -1551,6 +1627,10 @@ function validateResourceSchemaKeywords(id, path2, schema) {
|
|
|
1551
1627
|
if (isPlainObject4(sub)) validateResourceSchemaKeywords(id, `${path2}.properties.${key}`, sub);
|
|
1552
1628
|
}
|
|
1553
1629
|
}
|
|
1630
|
+
const additional = schema["additionalProperties"];
|
|
1631
|
+
if (isPlainObject4(additional)) {
|
|
1632
|
+
validateResourceSchemaKeywords(id, `${path2}.additionalProperties`, additional);
|
|
1633
|
+
}
|
|
1554
1634
|
const items = schema["items"];
|
|
1555
1635
|
if (isPlainObject4(items)) {
|
|
1556
1636
|
validateResourceSchemaKeywords(id, `${path2}.items`, items);
|
|
@@ -1600,6 +1680,23 @@ function validatePathParametersDeclared(id, url, input) {
|
|
|
1600
1680
|
}
|
|
1601
1681
|
const params = extractPathParameterNames(url);
|
|
1602
1682
|
if (params.length === 0) return;
|
|
1683
|
+
const firstBrace = url.indexOf("{");
|
|
1684
|
+
if (firstBrace !== -1) {
|
|
1685
|
+
const prefix = url.slice(0, firstBrace);
|
|
1686
|
+
let origin;
|
|
1687
|
+
try {
|
|
1688
|
+
const parsedPrefix = new URL(prefix);
|
|
1689
|
+
origin = parsedPrefix.hostname === "" ? void 0 : parsedPrefix.origin;
|
|
1690
|
+
} catch {
|
|
1691
|
+
}
|
|
1692
|
+
if (origin === void 0 || !prefix.startsWith(`${origin}/`)) {
|
|
1693
|
+
throw new CommerceError(
|
|
1694
|
+
"CONFIG_INVALID",
|
|
1695
|
+
`Resource "${id}" has a backend.url parameter before the end of the host ("${url}"). A caller-supplied value would choose which host the gateway calls, which is request forgery with the gateway's own network position. Parameters are supported inside the path and query only`,
|
|
1696
|
+
{ details: { path: `resources.${id}.backend.url`, resourceId: id } }
|
|
1697
|
+
);
|
|
1698
|
+
}
|
|
1699
|
+
}
|
|
1603
1700
|
const propertiesRaw = input?.["properties"];
|
|
1604
1701
|
const properties = isPlainObject4(propertiesRaw) ? propertiesRaw : {};
|
|
1605
1702
|
const requiredRaw = input?.["required"];
|
|
@@ -1755,7 +1852,7 @@ var SECRET_FIELD_NAMES = [
|
|
|
1755
1852
|
];
|
|
1756
1853
|
var REDACT_PATHS = [
|
|
1757
1854
|
"req.headers.authorization",
|
|
1758
|
-
'req.headers["
|
|
1855
|
+
'req.headers["payment-signature"]',
|
|
1759
1856
|
...SECRET_FIELD_NAMES,
|
|
1760
1857
|
...SECRET_FIELD_NAMES.map((name) => `*.${name}`)
|
|
1761
1858
|
];
|
|
@@ -1813,7 +1910,8 @@ function wrapPino(instance) {
|
|
|
1813
1910
|
return logger;
|
|
1814
1911
|
}
|
|
1815
1912
|
var CORS_METHODS = "GET,POST,DELETE,OPTIONS";
|
|
1816
|
-
var CORS_HEADERS = "content-type,
|
|
1913
|
+
var CORS_HEADERS = "content-type,payment-signature,x-request-id,authorization";
|
|
1914
|
+
var CORS_EXPOSED_HEADERS = "payment-required,payment-response,x-request-id";
|
|
1817
1915
|
function buildAccessControlHook(options) {
|
|
1818
1916
|
const allowedOrigins = new Set(options.allowedOrigins.map((origin) => origin.toLowerCase()));
|
|
1819
1917
|
const publicHostname = hostnameOf(options.publicBaseUrl);
|
|
@@ -1832,6 +1930,7 @@ function buildAccessControlHook(options) {
|
|
|
1832
1930
|
reply.header("access-control-allow-origin", origin);
|
|
1833
1931
|
reply.header("access-control-allow-methods", CORS_METHODS);
|
|
1834
1932
|
reply.header("access-control-allow-headers", CORS_HEADERS);
|
|
1933
|
+
reply.header("access-control-expose-headers", CORS_EXPOSED_HEADERS);
|
|
1835
1934
|
reply.header("vary", "Origin");
|
|
1836
1935
|
if (request.method === "OPTIONS") {
|
|
1837
1936
|
reply.status(204).send();
|
|
@@ -2142,13 +2241,17 @@ function createReadinessProbe(options, ttlMs = READINESS_TTL_MS) {
|
|
|
2142
2241
|
}
|
|
2143
2242
|
|
|
2144
2243
|
// src/gateway/well-known.ts
|
|
2145
|
-
var GATEWAY_SUPPORTED_SPEC = "agent-commerce/
|
|
2244
|
+
var GATEWAY_SUPPORTED_SPEC = "agent-commerce/v1.0.0";
|
|
2245
|
+
function publicHealth(health) {
|
|
2246
|
+
const { detail: _detail, ...rest } = health;
|
|
2247
|
+
return rest;
|
|
2248
|
+
}
|
|
2146
2249
|
async function buildWellKnownDocument(options) {
|
|
2147
2250
|
const adapters = await Promise.all(
|
|
2148
|
-
options.adapterRuntimes.map(async (runtime) =>
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
})
|
|
2251
|
+
options.adapterRuntimes.map(async (runtime) => {
|
|
2252
|
+
const health = await getAdapterHealth(runtime, options.clock);
|
|
2253
|
+
return { ...runtime.adapter.descriptor, health: publicHealth(health) };
|
|
2254
|
+
})
|
|
2152
2255
|
);
|
|
2153
2256
|
const x402 = options.config.payments.x402;
|
|
2154
2257
|
return {
|
|
@@ -2172,9 +2275,18 @@ async function buildWellKnownDocument(options) {
|
|
|
2172
2275
|
assetDecimals: x402.assetDecimals,
|
|
2173
2276
|
payTo: x402.payTo,
|
|
2174
2277
|
maxTimeoutSeconds: x402.maxTimeoutSeconds,
|
|
2175
|
-
// Never the signerPrivateKey
|
|
2176
|
-
//
|
|
2177
|
-
|
|
2278
|
+
// Never the signerPrivateKey, and never the facilitator URL:
|
|
2279
|
+
// a facilitator endpoint is a per-deployment operational detail
|
|
2280
|
+
// that can carry a tenant path or an API key, exactly like
|
|
2281
|
+
// `rpcUrl` above. Only whether it is local or remote is public.
|
|
2282
|
+
facilitator: { mode: x402.facilitator.mode },
|
|
2283
|
+
// Chain id 84532 is shared between the local dev chain and
|
|
2284
|
+
// public Base Sepolia, so the network id alone cannot say which
|
|
2285
|
+
// one a client is talking to. This can.
|
|
2286
|
+
mode: resolveDeploymentMode(
|
|
2287
|
+
requireNetworkProfile(x402.network, "payments.x402.network"),
|
|
2288
|
+
x402.facilitator.mode
|
|
2289
|
+
)
|
|
2178
2290
|
}
|
|
2179
2291
|
} : {}
|
|
2180
2292
|
}
|
|
@@ -2309,7 +2421,12 @@ async function handleInvoke(request, reply, options) {
|
|
|
2309
2421
|
};
|
|
2310
2422
|
const outcome = await options.pipeline.execute(canonicalRequest);
|
|
2311
2423
|
if (outcome.kind === "payment-required") {
|
|
2312
|
-
|
|
2424
|
+
const envelope = toPaymentRequiredEnvelope(outcome);
|
|
2425
|
+
if (envelope.payment.envelope !== void 0) {
|
|
2426
|
+
reply.header(PAYMENT_REQUIRED_HEADER, encodeHeaderDocument(envelope.payment.envelope));
|
|
2427
|
+
}
|
|
2428
|
+
reply.header("cache-control", "no-store");
|
|
2429
|
+
reply.status(402).send(envelope);
|
|
2313
2430
|
return;
|
|
2314
2431
|
}
|
|
2315
2432
|
if (outcome.payment) {
|
|
@@ -2329,7 +2446,7 @@ function errorPaymentSummary(error) {
|
|
|
2329
2446
|
const payment = error.details?.["payment"];
|
|
2330
2447
|
if (typeof payment !== "object" || payment === null) return void 0;
|
|
2331
2448
|
const rec = payment;
|
|
2332
|
-
const { status, provider, amount, currency, externalReference } = rec;
|
|
2449
|
+
const { status, provider, amount, currency, network, externalReference } = rec;
|
|
2333
2450
|
if (typeof status !== "string" || typeof provider !== "string" || typeof amount !== "string" || typeof currency !== "string") {
|
|
2334
2451
|
return void 0;
|
|
2335
2452
|
}
|
|
@@ -2338,18 +2455,24 @@ function errorPaymentSummary(error) {
|
|
|
2338
2455
|
provider,
|
|
2339
2456
|
amount,
|
|
2340
2457
|
currency,
|
|
2458
|
+
...typeof network === "string" ? { network } : {},
|
|
2341
2459
|
...typeof externalReference === "string" ? { externalReference } : {}
|
|
2342
2460
|
};
|
|
2343
2461
|
}
|
|
2462
|
+
function encodeHeaderDocument(document) {
|
|
2463
|
+
return Buffer.from(JSON.stringify(document), "utf8").toString("base64");
|
|
2464
|
+
}
|
|
2344
2465
|
function encodePaymentSummary(payment) {
|
|
2345
|
-
|
|
2466
|
+
return encodeHeaderDocument({
|
|
2467
|
+
success: payment.status === "settled",
|
|
2468
|
+
transaction: payment.externalReference ?? "",
|
|
2469
|
+
network: payment.network ?? "",
|
|
2346
2470
|
status: payment.status,
|
|
2347
2471
|
provider: payment.provider,
|
|
2348
2472
|
amount: payment.amount,
|
|
2349
2473
|
currency: payment.currency,
|
|
2350
2474
|
...payment.externalReference !== void 0 ? { externalReference: payment.externalReference } : {}
|
|
2351
|
-
};
|
|
2352
|
-
return Buffer.from(JSON.stringify(summary), "utf8").toString("base64");
|
|
2475
|
+
});
|
|
2353
2476
|
}
|
|
2354
2477
|
function parseLimit(value) {
|
|
2355
2478
|
if (Array.isArray(value)) return parseLimit(value[0]);
|
|
@@ -2683,6 +2806,12 @@ function createSqliteReceiptStore(options) {
|
|
|
2683
2806
|
);
|
|
2684
2807
|
}
|
|
2685
2808
|
}
|
|
2809
|
+
if (isFileBacked) {
|
|
2810
|
+
try {
|
|
2811
|
+
closeSync(openSync(path2, "a", 384));
|
|
2812
|
+
} catch {
|
|
2813
|
+
}
|
|
2814
|
+
}
|
|
2686
2815
|
const db = new Database(path2);
|
|
2687
2816
|
db.pragma("journal_mode = WAL");
|
|
2688
2817
|
db.pragma("foreign_keys = ON");
|