@devlab.group/agent-commerce 0.1.0-alpha.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.
@@ -0,0 +1,412 @@
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-LCk2TzGN.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-LCk2TzGN.js';
3
+ 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-C0goMgLK.js';
4
+ 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-C0goMgLK.js';
5
+ import { FastifyInstance } from 'fastify';
6
+ import 'node:http';
7
+
8
+ /**
9
+ * Typed domain error codes.
10
+ *
11
+ * FROZEN CONTRACT. Protocol adapters map these codes into their own response
12
+ * format. Never throw a bare string or untyped Error across a package boundary.
13
+ */
14
+ declare const COMMERCE_ERROR_CODES: readonly ["CONFIG_INVALID", "RESOURCE_NOT_FOUND", "INPUT_INVALID", "PAYMENT_REQUIRED", "PAYMENT_INVALID", "PAYMENT_REPLAYED", "PAYMENT_PROVIDER_UNAVAILABLE", "PAYMENT_SETTLEMENT_FAILED", "BACKEND_TIMEOUT", "BACKEND_ERROR", "PROTOCOL_UNSUPPORTED", "GATEWAY_BUSY", "STORAGE_ERROR", "INTERNAL_ERROR"];
15
+ type CommerceErrorCode = (typeof COMMERCE_ERROR_CODES)[number];
16
+ /**
17
+ * Canonical HTTP status for each error code.
18
+ *
19
+ * Paid resources fail closed: every payment failure maps to a 4xx/5xx status
20
+ * and never to a successful delivery.
21
+ */
22
+ declare const COMMERCE_ERROR_HTTP_STATUS: Readonly<Record<CommerceErrorCode, number>>;
23
+ /** Codes for which a client may reasonably retry the same request. */
24
+ declare const RETRYABLE_ERROR_CODES: ReadonlySet<CommerceErrorCode>;
25
+
26
+ /**
27
+ * The single error type crossing package boundaries.
28
+ *
29
+ * FROZEN CONTRACT.
30
+ */
31
+
32
+ interface CommerceErrorOptions {
33
+ /** Structured, non-secret detail safe to return to a client. */
34
+ readonly details?: Readonly<Record<string, unknown>>;
35
+ readonly cause?: unknown;
36
+ readonly requestId?: string;
37
+ readonly resourceId?: string;
38
+ /** Override the default HTTP status for this code. */
39
+ readonly httpStatus?: number;
40
+ }
41
+ /** Serialisable wire form of a CommerceError. */
42
+ interface CommerceErrorInfo {
43
+ readonly code: CommerceErrorCode;
44
+ readonly message: string;
45
+ readonly httpStatus: number;
46
+ readonly retryable: boolean;
47
+ readonly requestId?: string;
48
+ readonly resourceId?: string;
49
+ readonly details?: Readonly<Record<string, unknown>>;
50
+ }
51
+ declare class CommerceError extends Error {
52
+ readonly code: CommerceErrorCode;
53
+ readonly httpStatus: number;
54
+ readonly retryable: boolean;
55
+ readonly details?: Readonly<Record<string, unknown>>;
56
+ readonly requestId?: string;
57
+ readonly resourceId?: string;
58
+ constructor(code: CommerceErrorCode, message: string, options?: CommerceErrorOptions);
59
+ /** Non-secret, client-safe representation. */
60
+ toInfo(): CommerceErrorInfo;
61
+ }
62
+ declare function isCommerceError(value: unknown): value is CommerceError;
63
+ /**
64
+ * Normalise any thrown value into a CommerceError.
65
+ *
66
+ * Used at package boundaries so an unexpected failure never escapes untyped.
67
+ *
68
+ * SECURITY — do not "improve" this by propagating `value.message`.
69
+ * `CommerceError.message` is client-visible: it is serialised by `toInfo()`
70
+ * and `toErrorEnvelope()` and returned over HTTP and MCP. An arbitrary thrown
71
+ * Error's message routinely carries internal detail — database connection
72
+ * strings, internal hostnames, filesystem paths, backend URLs. Copying it into
73
+ * a client-facing field turns every unexpected exception into an information
74
+ * disclosure.
75
+ *
76
+ * The original value is preserved on `cause`, which is never serialised, so
77
+ * logs and diagnostics keep the full detail while clients get only the
78
+ * caller's deliberate, reviewed `fallbackMessage`.
79
+ *
80
+ * A caller that *wants* a specific client-facing message passes one:
81
+ * toCommerceError(err, 'BACKEND_ERROR', 'Backend call failed')
82
+ */
83
+ declare function toCommerceError(value: unknown, fallbackCode?: CommerceErrorCode, fallbackMessage?: string): CommerceError;
84
+
85
+ /**
86
+ * Gateway-defined wire envelopes.
87
+ *
88
+ * FROZEN CONTRACT. These are the *only* representations of "payment required"
89
+ * and "error" that leave the gateway, whichever protocol carries them. The HTTP
90
+ * routes, the MCP adapter and the demo buyer agent all use these helpers so the
91
+ * three can never drift apart.
92
+ */
93
+
94
+ /**
95
+ * Reserved input property carrying a payment proof on protocols that have no
96
+ * header channel (MCP tool calls). Over HTTP the `X-PAYMENT` header is used.
97
+ */
98
+ declare const PAYMENT_INPUT_FIELD = "_payment";
99
+ /** HTTP header carrying the payment proof. */
100
+ declare const PAYMENT_HEADER = "x-payment";
101
+ /** HTTP response header carrying the settlement reference. */
102
+ declare const PAYMENT_RESPONSE_HEADER = "x-payment-response";
103
+ /**
104
+ * Key under which a protocol adapter attaches a {@link DeliverySummary} to its
105
+ * result metadata — MCP's `CallToolResult._meta`, and anywhere else a protocol
106
+ * offers an out-of-band metadata channel.
107
+ *
108
+ * Frozen deliberately. The bug this whole mechanism exists to fix was two
109
+ * surfaces disagreeing about what a payer gets back; a key name enforced only
110
+ * by one side's tests would reintroduce exactly that drift. Producer and
111
+ * consumer both import this constant.
112
+ *
113
+ * Follows MCP's `<namespace>/<name>` convention for `_meta` keys.
114
+ */
115
+ declare const DELIVERY_SUMMARY_META_KEY = "agent-commerce/delivery";
116
+ interface PaymentRequiredEnvelope {
117
+ readonly status: 'payment-required';
118
+ readonly code: 'PAYMENT_REQUIRED';
119
+ readonly requestId: string;
120
+ readonly resourceId: string;
121
+ readonly message: string;
122
+ readonly payment: {
123
+ readonly provider: PaymentMethodName;
124
+ readonly version: string;
125
+ readonly amount: DecimalAmount;
126
+ readonly currency: string;
127
+ readonly destination: string;
128
+ readonly network?: string;
129
+ readonly asset?: string;
130
+ readonly expiresAt?: IsoTimestamp;
131
+ /** Provider-native requirement objects, passed through verbatim. */
132
+ readonly accepts: readonly Readonly<Record<string, unknown>>[];
133
+ };
134
+ }
135
+ interface ErrorEnvelope {
136
+ readonly status: 'error';
137
+ readonly code: CommerceErrorCode;
138
+ readonly message: string;
139
+ readonly retryable: boolean;
140
+ readonly requestId?: string;
141
+ readonly resourceId?: string;
142
+ readonly details?: Readonly<Record<string, unknown>>;
143
+ }
144
+ /**
145
+ * What a buyer learns about their own completed purchase.
146
+ *
147
+ * A payer is entitled to the record of their own transaction — the request it
148
+ * belonged to, whether it settled, and the settlement reference they can check
149
+ * on-chain. They are NOT entitled to the merchant's ledger, which is why
150
+ * `/api/receipts` is an operator route behind `server.adminToken`.
151
+ *
152
+ * Before this existed the two surfaces disagreed: HTTP callers received a
153
+ * settlement summary in `X-PAYMENT-RESPONSE` while MCP callers received
154
+ * nothing, so an MCP buyer's only route to their own receipt was the merchant's
155
+ * ledger. Both surfaces now emit this same shape.
156
+ */
157
+ interface DeliverySummary {
158
+ readonly requestId: string;
159
+ readonly resourceId: string;
160
+ readonly receiptId: string;
161
+ readonly deliveredAt: IsoTimestamp;
162
+ readonly payment?: {
163
+ readonly status: 'verified' | 'settled' | 'rejected';
164
+ readonly amount: DecimalAmount;
165
+ readonly currency: string;
166
+ /** Settlement reference, e.g. an on-chain transaction hash. */
167
+ readonly externalReference?: string;
168
+ readonly network?: string;
169
+ };
170
+ }
171
+ /** Build the payer-facing summary of a delivered outcome. */
172
+ declare function toDeliverySummary(outcome: DeliveredOutcome): DeliverySummary;
173
+ declare function toPaymentRequiredEnvelope(outcome: PaymentRequiredOutcome): PaymentRequiredEnvelope;
174
+ declare function toErrorEnvelope(error: CommerceError): ErrorEnvelope;
175
+ declare function isPaymentRequiredEnvelope(value: unknown): value is PaymentRequiredEnvelope;
176
+
177
+ /**
178
+ * Merchant backend execution boundary.
179
+ *
180
+ * The only place in the system allowed to make an outbound HTTP call to a
181
+ * merchant backend. Every call is bounded by a timeout; redirects are not
182
+ * followed (see docs/security.md).
183
+ */
184
+
185
+ interface BackendRequest {
186
+ readonly requestId: string;
187
+ readonly resourceId: string;
188
+ /** Validated resource input. */
189
+ readonly input: unknown;
190
+ }
191
+ interface BackendResponse {
192
+ readonly status: number;
193
+ readonly headers: Readonly<Record<string, string>>;
194
+ readonly body: unknown;
195
+ readonly durationMs: number;
196
+ }
197
+ interface BackendExecutor {
198
+ /**
199
+ * Call the merchant backend.
200
+ *
201
+ * Must throw `CommerceError('BACKEND_TIMEOUT')` on timeout and
202
+ * `CommerceError('BACKEND_ERROR')` for non-2xx responses and transport
203
+ * failures. Must never throw an untyped error.
204
+ */
205
+ call(handler: BackendHandler, request: BackendRequest): Promise<BackendResponse>;
206
+ }
207
+
208
+ /**
209
+ * Persistence boundary for receipts, events and payment attempts.
210
+ *
211
+ * Implemented by src/storage/receipts. Core depends only on this
212
+ * interface, never on SQLite.
213
+ */
214
+
215
+ interface PaymentAttemptReservation {
216
+ readonly requestId: string;
217
+ readonly resourceId: string;
218
+ readonly provider: string;
219
+ /** Must be unique across the store — see PaymentResult.replayKey. */
220
+ readonly replayKey: string;
221
+ readonly amount: string;
222
+ readonly currency: string;
223
+ readonly payer?: string;
224
+ readonly payee?: string;
225
+ }
226
+ interface PaymentAttemptUpdate {
227
+ readonly replayKey: string;
228
+ readonly status: PaymentAttempt['status'];
229
+ readonly externalReference?: string;
230
+ readonly rejectionReason?: string;
231
+ }
232
+ interface ListOptions {
233
+ readonly limit?: number;
234
+ readonly requestId?: string;
235
+ }
236
+ interface ReceiptStore {
237
+ init(): Promise<void>;
238
+ /** Append a canonical event. Must not throw into the caller's flow. */
239
+ appendEvent(event: CommerceEvent): Promise<void>;
240
+ /**
241
+ * Atomically claim a payment authorisation before settlement.
242
+ *
243
+ * Throws `CommerceError('PAYMENT_REPLAYED')` if `replayKey` was already
244
+ * reserved. This is the gateway's replay defence and is required for every
245
+ * paid request.
246
+ */
247
+ reservePaymentAttempt(reservation: PaymentAttemptReservation): Promise<PaymentAttempt>;
248
+ updatePaymentAttempt(update: PaymentAttemptUpdate): Promise<void>;
249
+ saveReceipt(receipt: CommerceReceipt): Promise<void>;
250
+ getReceipt(id: string): Promise<CommerceReceipt | undefined>;
251
+ listReceipts(options?: ListOptions): Promise<readonly CommerceReceipt[]>;
252
+ /**
253
+ * Total receipts held, ignoring any list limit.
254
+ *
255
+ * `listReceipts` clamps to a bounded page (a store-level invariant, so an
256
+ * unbounded `limit` cannot dump the ledger). Counting by measuring the length
257
+ * of a list therefore silently saturates at the clamp — `doctor` reported
258
+ * "receipts=500" forever once 500 was passed. A diagnostic that quietly stops
259
+ * counting is worse than one that admits it cannot.
260
+ */
261
+ countReceipts(): Promise<number>;
262
+ /**
263
+ * Receipts whose delivery did not succeed — `backendStatus` outside 2xx.
264
+ *
265
+ * A paid-but-undelivered purchase is the one row a merchant must notice:
266
+ * settlement is final and v0.1 has no refunds, so *seeing* it is the only
267
+ * remedy available. Counted rather than listed for the same reason as
268
+ * {@link countReceipts} — `listReceipts` is clamped, so counting by list
269
+ * length silently saturates.
270
+ *
271
+ * In practice this is exactly "paid but undelivered", not "any failed
272
+ * delivery": the pipeline only ever persists a receipt when a payment was
273
+ * attempted (`paymentResult !== undefined`), so a free resource whose
274
+ * backend call fails throws without writing a receipt at all — there is no
275
+ * row for `countUndeliveredReceipts` to see. Free-resource failures are
276
+ * therefore *not* counted here, however natural it is to read the name as
277
+ * saying they are. Writing a receipt for a failed free delivery too would be
278
+ * a more complete ledger, but it changes what this count means — that is
279
+ * a product decision for after the alpha, not something to do here.
280
+ */
281
+ countUndeliveredReceipts(): Promise<number>;
282
+ listEvents(options?: ListOptions): Promise<readonly CommerceEvent[]>;
283
+ listPaymentAttempts(options?: ListOptions): Promise<readonly PaymentAttempt[]>;
284
+ readonly descriptor: AdapterDescriptor;
285
+ health(): Promise<AdapterHealth>;
286
+ close(): Promise<void>;
287
+ }
288
+
289
+ /**
290
+ * `config.yaml` schema, validation and normalisation into the
291
+ * canonical `GatewayConfig` (docs/contracts.md).
292
+ *
293
+ * Two-phase validation:
294
+ * 1. Zod validates *shape* (types, required fields, unknown-key rejection).
295
+ * Numeric/boolean leaves accept either their native type or a string
296
+ * (env substitution always produces a string), and are left as-is here —
297
+ * Zod's typed transforms have surprising inference interactions with
298
+ * `.strict()` objects, so numeric/boolean coercion is done explicitly,
299
+ * in plain TypeScript, in the normalisation pass below.
300
+ * 2. A manual business-rule pass validates cross-references that need a
301
+ * specific, actionable message (duplicate ids, disabled protocols/payment
302
+ * methods, dynamic pricing, destination-address plausibility) and
303
+ * performs the numeric/boolean coercion.
304
+ *
305
+ * Env substitution (`${VAR}` / `${VAR:-default}`) runs over the raw parsed
306
+ * value *before* either phase, so numeric/boolean fields can be templated too.
307
+ * One exception, deliberate: `checkVersion` runs **before** substitution, so
308
+ * `version:` cannot itself be templated. It is the compatibility gate that
309
+ * decides whether this parser understands the document at all; resolving
310
+ * environment variables to find out which schema version to expect would be
311
+ * backwards. The order is intended; `version:` is the one exception to the
312
+ * substitution rule, which this sentence exists to record.
313
+ */
314
+
315
+ interface GatewayConfig {
316
+ readonly version: 1;
317
+ readonly merchant: {
318
+ readonly id: string;
319
+ readonly name: string;
320
+ readonly publicBaseUrl: string;
321
+ };
322
+ readonly server: {
323
+ readonly port: number;
324
+ readonly host: string;
325
+ readonly adminToken?: string;
326
+ readonly allowedOrigins: readonly string[];
327
+ };
328
+ readonly storage: {
329
+ readonly receipts: {
330
+ readonly driver: 'sqlite';
331
+ readonly path: string;
332
+ };
333
+ };
334
+ readonly protocols: {
335
+ readonly http: {
336
+ readonly enabled: boolean;
337
+ };
338
+ readonly mcp: {
339
+ readonly enabled: boolean;
340
+ readonly mountPath: string;
341
+ };
342
+ };
343
+ /** Canonical resources, already normalised. */
344
+ readonly resources: readonly CommerceResource[];
345
+ readonly payments: {
346
+ readonly x402?: {
347
+ readonly enabled: boolean;
348
+ readonly network: string;
349
+ readonly rpcUrl: string;
350
+ readonly asset: string;
351
+ readonly assetName: string;
352
+ readonly assetVersion: string;
353
+ readonly assetDecimals: number;
354
+ readonly payTo: string;
355
+ readonly maxTimeoutSeconds: number;
356
+ readonly facilitator: {
357
+ readonly mode: 'local';
358
+ readonly signerPrivateKey: string;
359
+ } | {
360
+ readonly mode: 'remote';
361
+ readonly url: string;
362
+ };
363
+ };
364
+ };
365
+ }
366
+ declare function parseConfig(raw: unknown, env: NodeJS.ProcessEnv): GatewayConfig;
367
+
368
+ interface LoadConfigOptions {
369
+ readonly path?: string;
370
+ readonly env?: NodeJS.ProcessEnv;
371
+ readonly cwd?: string;
372
+ }
373
+ declare function loadConfig(options?: LoadConfigOptions): Promise<GatewayConfig>;
374
+
375
+ /**
376
+ * `createGateway()` — the Fastify server. Fully usable (and tested) with
377
+ * fakes, without `main.ts` (docs/contracts.md "Composition root").
378
+ */
379
+
380
+ interface GatewayOptions {
381
+ readonly config: GatewayConfig;
382
+ readonly store: ReceiptStore;
383
+ readonly paymentProviders: readonly PaymentProvider[];
384
+ readonly protocolAdapters: readonly ProtocolAdapter[];
385
+ readonly logger?: Logger;
386
+ readonly clock?: Clock;
387
+ readonly ids?: IdGenerator;
388
+ /** Override for tests. */
389
+ readonly backend?: BackendExecutor;
390
+ }
391
+ interface GatewayInstance {
392
+ readonly pipeline: ExecutionPipeline;
393
+ readonly resources: ResourceRegistry;
394
+ listen(): Promise<{
395
+ url: string;
396
+ }>;
397
+ close(): Promise<void>;
398
+ /** Fastify instance, for `.inject()` in tests. */
399
+ readonly server: FastifyInstance;
400
+ }
401
+ declare function createGateway(options: GatewayOptions): Promise<GatewayInstance>;
402
+
403
+ interface SqliteReceiptStoreOptions {
404
+ /** File path, or ':memory:' for tests. Parent directory is created if missing. */
405
+ readonly path: string;
406
+ readonly logger?: Logger;
407
+ readonly clock?: Clock;
408
+ readonly ids?: IdGenerator;
409
+ }
410
+ declare function createSqliteReceiptStore(options: SqliteReceiptStoreOptions): ReceiptStore;
411
+
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 };