@envarai/envarpay 0.2.0-alpha.1 → 0.2.0-alpha.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commerce/buyer-store.d.ts +8 -0
- package/dist/commerce/buyer.d.ts +5 -2
- package/dist/commerce/buyer.js +129 -5
- package/dist/commerce/cli.js +7 -1
- package/dist/commerce/config.js +18 -0
- package/dist/commerce/operations.d.ts +16 -0
- package/dist/commerce/operations.js +33 -0
- package/dist/commerce/peer-proxy.js +11 -5
- package/dist/commerce/server.d.ts +1 -0
- package/dist/commerce/server.js +26 -8
- package/dist/commerce/store.d.ts +2 -0
- package/dist/commerce/store.js +7 -0
- package/dist/commerce/types.d.ts +22 -1
- package/package.json +1 -1
- package/schemas/buyer-policy.schema.json +105 -0
|
@@ -15,6 +15,8 @@ export interface BuyerQuote {
|
|
|
15
15
|
inputDigest: string;
|
|
16
16
|
termsDigest: string;
|
|
17
17
|
quoteId: string;
|
|
18
|
+
/** Absent for the original seller-advertised EnvarPay contract. */
|
|
19
|
+
termsSource?: 'local-policy';
|
|
18
20
|
}
|
|
19
21
|
export interface BuyerRecord {
|
|
20
22
|
id: string;
|
|
@@ -29,6 +31,12 @@ export interface BuyerRecord {
|
|
|
29
31
|
quote: BuyerQuote;
|
|
30
32
|
input: Input;
|
|
31
33
|
inputSchema: Record<string, unknown>;
|
|
34
|
+
peerMode?: 'standard-a2a';
|
|
35
|
+
standardBinding?: {
|
|
36
|
+
policyDigest: string;
|
|
37
|
+
bodyDigest: string;
|
|
38
|
+
challengeDigest: string;
|
|
39
|
+
};
|
|
32
40
|
continuations?: {
|
|
33
41
|
messageId: string;
|
|
34
42
|
input: Input;
|
package/dist/commerce/buyer.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ClientEvmSigner } from '@x402/evm';
|
|
2
2
|
import type { PaymentPayload, PaymentRequirements, SettleResponse } from '@x402/core/types';
|
|
3
|
-
import { type BuyerPolicy, type Input } from './types.js';
|
|
3
|
+
import { type BuyerPolicy, type Input, type StandardA2APeer } from './types.js';
|
|
4
4
|
import { CredentialVault } from './vault.js';
|
|
5
5
|
import { BuyerStore, type BuyerQuote } from './buyer-store.js';
|
|
6
6
|
import { type MppBuyerOptions, type MppReceipt } from './mpp-client.js';
|
|
@@ -78,9 +78,10 @@ export declare class CommerceBuyer {
|
|
|
78
78
|
policyVersion: 1;
|
|
79
79
|
paymentsEnabled: boolean;
|
|
80
80
|
approval: "per_purchase" | "within_preapproved_limits";
|
|
81
|
-
peers: ({
|
|
81
|
+
peers: (StandardA2APeer | {
|
|
82
82
|
id: string;
|
|
83
83
|
cardUrl: string;
|
|
84
|
+
mode?: "envarpay";
|
|
84
85
|
protocol: "x402" | "mpp";
|
|
85
86
|
currency: string;
|
|
86
87
|
recipient: string;
|
|
@@ -88,6 +89,7 @@ export declare class CommerceBuyer {
|
|
|
88
89
|
} | {
|
|
89
90
|
id: string;
|
|
90
91
|
cardUrl: string;
|
|
92
|
+
mode?: "envarpay";
|
|
91
93
|
protocol: "free";
|
|
92
94
|
currency: null;
|
|
93
95
|
recipient: null;
|
|
@@ -102,6 +104,7 @@ export declare class CommerceBuyer {
|
|
|
102
104
|
get(caller: string, id: string): BuyerSnapshot;
|
|
103
105
|
preview(caller: string, input: BuyerPreviewInput): Promise<BuyerSnapshot>;
|
|
104
106
|
private prepare;
|
|
107
|
+
private prepareStandard;
|
|
105
108
|
private prepareFree;
|
|
106
109
|
private replayFree;
|
|
107
110
|
private prepareMpp;
|
package/dist/commerce/buyer.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { findDefaultAsset } from '@x402/evm';
|
|
2
2
|
import { randomBytes, randomUUID, timingSafeEqual } from 'node:crypto';
|
|
3
|
-
import { AgentCard, SendMessageRequest, GetTaskRequest, Task, TaskState } from '@a2a-js/sdk';
|
|
3
|
+
import { AgentCard, SendMessageRequest, GetTaskRequest, Task, TaskState, Message, Role } from '@a2a-js/sdk';
|
|
4
4
|
import { ClientFactory, JsonRpcTransportFactory } from '@a2a-js/sdk/client';
|
|
5
5
|
import { x402Client } from '@x402/core/client';
|
|
6
6
|
import { decodePaymentRequiredHeader, decodePaymentResponseHeader, encodePaymentSignatureHeader } from '@x402/core/http';
|
|
@@ -67,6 +67,31 @@ function noExternalRefs(value) {
|
|
|
67
67
|
noExternalRefs(child);
|
|
68
68
|
}
|
|
69
69
|
}
|
|
70
|
+
function standardPolicyDigest(peer) {
|
|
71
|
+
return digest({ mode: peer.mode, cardUrl: peer.cardUrl, endpoint: peer.endpoint, authentication: peer.authentication,
|
|
72
|
+
protocol: peer.protocol, currency: peer.currency, recipient: peer.recipient, localContract: peer.localContract });
|
|
73
|
+
}
|
|
74
|
+
function standardRequirements(peer, required) {
|
|
75
|
+
let resource;
|
|
76
|
+
try {
|
|
77
|
+
resource = new URL(required.resource?.url ?? '');
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
throw new CommerceError('quote_mismatch', 'Standard payment resource must name the approved A2A endpoint');
|
|
81
|
+
}
|
|
82
|
+
const candidates = Array.isArray(required.accepts) ? required.accepts.filter(r => {
|
|
83
|
+
if (!r || r.scheme !== 'exact' || typeof r.asset !== 'string' || typeof r.payTo !== 'string' || `${r.network}/erc20:${r.asset.toLowerCase()}` !== peer.currency || r.payTo.toLowerCase() !== peer.recipient || r.amount !== peer.localContract.amount || !Number.isInteger(r.maxTimeoutSeconds) || r.maxTimeoutSeconds < 1 || r.maxTimeoutSeconds > 300)
|
|
84
|
+
return false;
|
|
85
|
+
const asset = findDefaultAsset(r.asset, r.network), extra = r.extra;
|
|
86
|
+
return !!asset && !!extra && typeof extra === 'object' && !Array.isArray(extra)
|
|
87
|
+
&& (extra.assetTransferMethod === undefined || extra.assetTransferMethod === 'eip3009')
|
|
88
|
+
&& (extra.paymentFlow === undefined || extra.paymentFlow === 'upfront')
|
|
89
|
+
&& extra.name === asset.name && extra.version === asset.version;
|
|
90
|
+
}) : [];
|
|
91
|
+
if (required.x402Version !== 2 || resource.href !== peer.endpoint || resource.username || resource.password || resource.hash || candidates.length !== 1)
|
|
92
|
+
throw new CommerceError('quote_mismatch', 'Native challenge differs from the exact locally approved resource, price, asset, payee or timeout');
|
|
93
|
+
return candidates[0];
|
|
94
|
+
}
|
|
70
95
|
/** Independent x402 wallet; payment and execution state are deliberately separate. */
|
|
71
96
|
export class CommerceBuyer {
|
|
72
97
|
options;
|
|
@@ -86,8 +111,8 @@ export class CommerceBuyer {
|
|
|
86
111
|
if (this.policy.peers.some(p => p.protocol === 'mpp') && (!options.mpp || !options.mpp.payer || !/^pm_[A-Za-z0-9]+$/.test(options.mpp.paymentMethod) || !['test', 'live'].includes(options.mpp.mode) || typeof options.mpp.createToken !== 'function' || typeof options.mpp.verifyReceipt !== 'function'))
|
|
87
112
|
throw new CommerceError('mpp_provider_missing', 'MPP peers require explicit token creation and independent provider verification');
|
|
88
113
|
for (const peer of this.policy.peers)
|
|
89
|
-
if (!options.peerTokens[peer.id] || options.peerTokens[peer.id].length < 16)
|
|
90
|
-
throw new CommerceError('peer_credentials', 'Each
|
|
114
|
+
if (!(peer.mode === 'standard-a2a' && peer.authentication === 'none') && (!options.peerTokens[peer.id] || options.peerTokens[peer.id].length < 16))
|
|
115
|
+
throw new CommerceError('peer_credentials', 'Each authenticated peer needs its own authentication token');
|
|
91
116
|
}
|
|
92
117
|
/** Background observation only; never signs, replays credentials or changes reservations. */
|
|
93
118
|
start(intervalMs = 2000) {
|
|
@@ -117,6 +142,11 @@ export class CommerceBuyer {
|
|
|
117
142
|
}
|
|
118
143
|
endpoint(peer) {
|
|
119
144
|
const explicit = this.options.peerEndpoints?.[peer.id];
|
|
145
|
+
if (peer.mode === 'standard-a2a') {
|
|
146
|
+
if (explicit && explicit !== peer.endpoint)
|
|
147
|
+
throw new CommerceError('endpoint_policy', 'A standard peer endpoint must match its locally reviewed policy');
|
|
148
|
+
return peer.endpoint;
|
|
149
|
+
}
|
|
120
150
|
const card = new URL(peer.cardUrl);
|
|
121
151
|
if (!explicit && (!card.pathname.endsWith('/agent-card.json') || card.search))
|
|
122
152
|
throw new CommerceError('endpoint_policy', 'Configure an explicit peer endpoint for this Agent Card URL');
|
|
@@ -133,7 +163,10 @@ export class CommerceBuyer {
|
|
|
133
163
|
if ((request.url === peer.cardUrl && request.method !== 'GET') || (request.url === endpoint && request.method !== 'POST'))
|
|
134
164
|
throw new CommerceError('peer_scope', 'Unsupported peer operation');
|
|
135
165
|
const headers = new Headers(request.headers);
|
|
136
|
-
|
|
166
|
+
if (peer.mode === 'standard-a2a' && peer.authentication === 'none')
|
|
167
|
+
headers.delete('Authorization');
|
|
168
|
+
else
|
|
169
|
+
headers.set('Authorization', `Bearer ${this.options.peerTokens[peer.id]}`);
|
|
137
170
|
headers.set('A2A-Version', '1.0');
|
|
138
171
|
const response = await this.fetchImpl(new Request(request, { headers, redirect: 'error', signal: AbortSignal.any([request.signal, AbortSignal.timeout(30000)]) }));
|
|
139
172
|
if (response.status >= 300 && response.status < 400)
|
|
@@ -183,7 +216,11 @@ export class CommerceBuyer {
|
|
|
183
216
|
if (!response.ok)
|
|
184
217
|
throw new CommerceError('peer_card', 'Unable to read allowed Agent Card');
|
|
185
218
|
const card = AgentCard.fromJSON(await response.json());
|
|
186
|
-
if (!card.supportedInterfaces.some(i => i.protocolVersion === '1.0' && i.protocolBinding === 'JSONRPC' && i.url === endpoint)
|
|
219
|
+
if (!card.supportedInterfaces.some(i => i.protocolVersion === '1.0' && i.protocolBinding === 'JSONRPC' && i.url === endpoint))
|
|
220
|
+
throw new CommerceError('peer_interface', 'Agent Card must expose the approved A2A 1.0 JSONRPC endpoint');
|
|
221
|
+
if (peer.mode === 'standard-a2a')
|
|
222
|
+
return this.prepareStandard(caller, input, fingerprint, peer, card);
|
|
223
|
+
if (card.supportedInterfaces.some(i => i.url !== endpoint))
|
|
187
224
|
throw new CommerceError('peer_interface', 'Agent Card must expose only the approved A2A 1.0 JSONRPC endpoint');
|
|
188
225
|
const advertised = card.capabilities?.extensions.find(e => e.uri === COMMERCE)?.params;
|
|
189
226
|
if (!advertised || advertised.offerId !== input.offerId || !advertised.serviceId || !Number.isSafeInteger(advertised.serviceRevision) || advertised.serviceRevision < 1 || !/^[0-9a-f]{64}$/.test(advertised.termsDigest ?? '') || !advertised.contract?.inputSchema || (peer.protocol !== 'free' && (advertised.collection?.kind !== 'upfront' || advertised.payment?.adapter !== peer.protocol)))
|
|
@@ -247,6 +284,63 @@ export class CommerceBuyer {
|
|
|
247
284
|
};
|
|
248
285
|
return this.snapshot(this.options.store.insert({ protocol: 'x402', caller, peerId: peer.id, cardUrl: peer.cardUrl, endpoint, messageId: input.messageId, fingerprint, input: structuredClone(input.input), inputSchema: structuredClone(advertised.contract.inputSchema), body: originalBody, quoteToken: randomBytes(32).toString('base64url'), quote: frozenQuote, required, requirements: candidates[0] }));
|
|
249
286
|
}
|
|
287
|
+
async prepareStandard(caller, input, fingerprint, peer, card) {
|
|
288
|
+
const contract = peer.localContract;
|
|
289
|
+
if (card.securityRequirements.length && !card.securityRequirements.some(requirement => {
|
|
290
|
+
const names = Object.keys(requirement.schemes);
|
|
291
|
+
if (!names.length)
|
|
292
|
+
return true;
|
|
293
|
+
if (peer.authentication !== 'bearer' || names.length !== 1)
|
|
294
|
+
return false;
|
|
295
|
+
const name = names[0], scheme = card.securitySchemes[name]?.scheme;
|
|
296
|
+
return scheme?.$case === 'httpAuthSecurityScheme' && scheme.value.scheme.toLowerCase() === 'bearer' && !requirement.schemes[name].list.length;
|
|
297
|
+
}))
|
|
298
|
+
throw new CommerceError('peer_authentication', 'Agent Card authentication is not satisfied by the locally approved none/bearer mode');
|
|
299
|
+
if (input.offerId !== contract.offerId)
|
|
300
|
+
throw new CommerceError('offer_not_allowed', 'Select the locally reviewed standard-peer offer');
|
|
301
|
+
const ajv = new Ajv2020({ strict: true });
|
|
302
|
+
addFormats(ajv);
|
|
303
|
+
if (!ajv.compile(contract.inputSchema)(input.input))
|
|
304
|
+
throw new CommerceError('invalid_input', 'Input does not satisfy the locally reviewed schema');
|
|
305
|
+
const amount = atomic(contract.amount).toString();
|
|
306
|
+
if (atomic(amount) > atomic(peer.maxPerPurchase))
|
|
307
|
+
throw new CommerceError('purchase_limit', 'Local price exceeds the per-purchase limit');
|
|
308
|
+
let body = '', required;
|
|
309
|
+
const capture = async (value, init) => {
|
|
310
|
+
const request = new Request(value, init);
|
|
311
|
+
body = await request.clone().text();
|
|
312
|
+
if (Buffer.byteLength(body) > 1024 * 1024)
|
|
313
|
+
throw new CommerceError('request_too_large', 'Purchase input exceeds 1 MiB');
|
|
314
|
+
const response = await this.scopedFetch(peer, peer.endpoint)(request);
|
|
315
|
+
if (response.status !== 402)
|
|
316
|
+
throw new CommerceError('paid_quote_required', 'The operator-approved paid-only endpoint must return 402 before any authorization');
|
|
317
|
+
const header = response.headers.get('PAYMENT-REQUIRED');
|
|
318
|
+
if (!header || header.length > 65536)
|
|
319
|
+
throw new CommerceError('invalid_challenge', 'Missing or oversized native x402 challenge');
|
|
320
|
+
required = decodePaymentRequiredHeader(header);
|
|
321
|
+
throw new CommerceError('preview_captured', 'Native standard payment challenge captured');
|
|
322
|
+
};
|
|
323
|
+
// Ignore every advertised alternative; only the operator-selected interface may receive requests.
|
|
324
|
+
const selected = AgentCard.fromJSON({ ...AgentCard.toJSON(card), supportedInterfaces: [{ url: peer.endpoint, protocolBinding: 'JSONRPC', protocolVersion: '1.0' }] });
|
|
325
|
+
const client = await new ClientFactory({ transports: [new JsonRpcTransportFactory({ fetchImpl: capture })] }).createFromAgentCard(selected);
|
|
326
|
+
try {
|
|
327
|
+
await client.sendMessage(SendMessageRequest.fromJSON({ message: { messageId: input.messageId, role: 'ROLE_USER', parts: [{ data: input.input }] }, configuration: { returnImmediately: true } }));
|
|
328
|
+
}
|
|
329
|
+
catch (error) {
|
|
330
|
+
if (!required)
|
|
331
|
+
throw error;
|
|
332
|
+
}
|
|
333
|
+
if (!required || !body)
|
|
334
|
+
throw new CommerceError('invalid_challenge', 'The approved endpoint did not return a native payment challenge');
|
|
335
|
+
const requirements = standardRequirements(peer, required), policyDigest = standardPolicyDigest(peer);
|
|
336
|
+
const quote = { quoteId: randomUUID(), messageId: input.messageId, payer: this.options.signer.address.toLowerCase(), amount,
|
|
337
|
+
currency: peer.currency, recipient: peer.recipient, expiresAt: new Date(Date.now() + requirements.maxTimeoutSeconds * 1000).toISOString(),
|
|
338
|
+
serviceId: peer.id, serviceRevision: contract.revision, offerId: contract.offerId, inputDigest: digest(input.input), termsDigest: policyDigest, termsSource: 'local-policy' };
|
|
339
|
+
return this.snapshot(this.options.store.insert({ protocol: 'x402', peerMode: 'standard-a2a', caller, peerId: peer.id, cardUrl: peer.cardUrl, endpoint: peer.endpoint,
|
|
340
|
+
messageId: input.messageId, fingerprint, input: structuredClone(input.input), inputSchema: structuredClone(contract.inputSchema), body,
|
|
341
|
+
quoteToken: randomBytes(32).toString('base64url'), quote, required, requirements,
|
|
342
|
+
standardBinding: { policyDigest, bodyDigest: digest(body), challengeDigest: digest(required) } }));
|
|
343
|
+
}
|
|
250
344
|
prepareFree(caller, input, fingerprint, peer, endpoint, advertised) {
|
|
251
345
|
if (advertised.pricing?.kind !== 'free' || advertised.collection?.kind !== 'none' || advertised.payment !== null)
|
|
252
346
|
throw new CommerceError('free_offer_required', 'This peer permits explicit free offers only');
|
|
@@ -395,6 +489,17 @@ export class CommerceBuyer {
|
|
|
395
489
|
const peer = this.peer(record.cardUrl);
|
|
396
490
|
if (peer.id !== record.peerId || this.endpoint(peer) !== record.endpoint || peer.currency !== record.quote.currency || peer.recipient !== record.quote.recipient || (peer.protocol !== 'free' && atomic(record.quote.amount) > atomic(peer.maxPerPurchase)) || record.quote.payer !== (peer.protocol === 'free' ? record.caller : peer.protocol === 'mpp' ? this.options.mpp?.payer : this.options.signer?.address.toLowerCase()) || (record.protocol === 'mpp' && record.mppMode !== this.options.mpp?.mode))
|
|
397
491
|
throw new CommerceError('policy_changed', 'Original purchase no longer matches local wallet policy');
|
|
492
|
+
if ((record.peerMode === 'standard-a2a') !== (peer.mode === 'standard-a2a'))
|
|
493
|
+
throw new CommerceError('policy_changed', 'Original peer protocol profile changed');
|
|
494
|
+
if (record.peerMode === 'standard-a2a') {
|
|
495
|
+
if (peer.mode !== 'standard-a2a' || !record.standardBinding || record.standardBinding.policyDigest !== standardPolicyDigest(peer))
|
|
496
|
+
throw new CommerceError('policy_changed', 'Original local standard-peer contract or authentication changed');
|
|
497
|
+
const quote = record.quote, contract = peer.localContract;
|
|
498
|
+
if (quote.termsSource !== 'local-policy' || quote.termsDigest !== record.standardBinding.policyDigest || quote.inputDigest !== digest(record.input) || quote.messageId !== record.messageId || quote.amount !== contract.amount || quote.serviceId !== peer.id || quote.serviceRevision !== contract.revision || quote.offerId !== contract.offerId || digest(record.inputSchema) !== digest(contract.inputSchema))
|
|
499
|
+
throw new CommerceError('recovery_conflict', 'Local review quote no longer matches its frozen standard-peer contract and input');
|
|
500
|
+
if (record.standardBinding.bodyDigest !== digest(record.body) || record.standardBinding.challengeDigest !== digest(record.required) || digest(standardRequirements(peer, record.required)) !== digest(record.requirements))
|
|
501
|
+
throw new CommerceError('recovery_conflict', 'Original standard request or native challenge changed');
|
|
502
|
+
}
|
|
398
503
|
if (signing && peer.protocol !== 'free' && !this.policy.paymentsEnabled)
|
|
399
504
|
throw new CommerceError('payments_disabled', 'Enable payments in local wallet policy before confirming');
|
|
400
505
|
return peer;
|
|
@@ -470,6 +575,8 @@ export class CommerceBuyer {
|
|
|
470
575
|
async verify(record, saved, receipt) {
|
|
471
576
|
if (record.paymentState === 'confirmed')
|
|
472
577
|
return record;
|
|
578
|
+
if (record.peerMode === 'standard-a2a' && receipt && (receipt.network !== saved.requirements.network || (receipt.payer && receipt.payer.toLowerCase() !== record.quote.payer)))
|
|
579
|
+
throw new CommerceError('receipt_binding', 'Receipt differs from the original standard-peer chain or payer');
|
|
473
580
|
if (receipt)
|
|
474
581
|
record = this.options.store.update(record.id, { receipt });
|
|
475
582
|
if (!receipt?.transaction && record.fromBlock && this.options.findOriginalReceipt)
|
|
@@ -505,8 +612,16 @@ export class CommerceBuyer {
|
|
|
505
612
|
}
|
|
506
613
|
catch { /* retain the original receipt and reservation for reconciliation */ }
|
|
507
614
|
const raw = await response.json();
|
|
615
|
+
if (record.peerMode === 'standard-a2a' && (raw.jsonrpc !== '2.0' || raw.id !== JSON.parse(record.body).id))
|
|
616
|
+
throw new CommerceError('rpc_response_binding', 'Peer response does not match the original standard JSON-RPC request');
|
|
508
617
|
if (response.ok && raw.result?.task)
|
|
509
618
|
record = this.task(record, raw.result.task);
|
|
619
|
+
else if (record.peerMode === 'standard-a2a' && response.ok && raw.result?.message) {
|
|
620
|
+
const message = Message.fromJSON(raw.result.message);
|
|
621
|
+
if (!message.messageId || message.role !== Role.ROLE_AGENT || !message.parts.length)
|
|
622
|
+
throw new CommerceError('message_binding', 'Peer returned an invalid A2A response message');
|
|
623
|
+
record = this.options.store.update(record.id, { result: { message: Message.toJSON(message) }, executionState: 'completed', state: record.paymentState === 'confirmed' ? 'completed' : 'unknown' });
|
|
624
|
+
}
|
|
510
625
|
else
|
|
511
626
|
record = this.options.store.update(record.id, { state: 'unknown', errorCode: 'original_purchase_pending' });
|
|
512
627
|
return this.snapshot(record);
|
|
@@ -529,6 +644,8 @@ export class CommerceBuyer {
|
|
|
529
644
|
}
|
|
530
645
|
async continue(caller, id, input) {
|
|
531
646
|
const record = this.record(caller, id);
|
|
647
|
+
if (record.peerMode === 'standard-a2a')
|
|
648
|
+
throw new CommerceError('standard_continuation_unavailable', 'A standard peer has not promised included clarification; inspect its original Task without a new submission');
|
|
532
649
|
if (typeof input.messageId !== 'string' || !input.messageId || input.messageId.length > 128 || input.messageId === record.messageId || !input.input || typeof input.input !== 'object' || Array.isArray(input.input))
|
|
533
650
|
throw new CommerceError('invalid_continuation', 'A distinct stable message ID and full object input are required');
|
|
534
651
|
const running = this.active.get(id);
|
|
@@ -621,6 +738,13 @@ export class CommerceBuyer {
|
|
|
621
738
|
if (original.task?.id) {
|
|
622
739
|
return this.observe(original);
|
|
623
740
|
}
|
|
741
|
+
if (original.peerMode === 'standard-a2a') {
|
|
742
|
+
// A2A message IDs do not guarantee idempotent execution at an unrelated seller.
|
|
743
|
+
// Keep the exact original authorization, but never POST it again after an ambiguous outcome.
|
|
744
|
+
if (original.paymentState === 'confirmed' && original.executionState === 'completed' && original.result && typeof original.result === 'object' && 'message' in original.result)
|
|
745
|
+
return this.snapshot(this.options.store.update(id, { state: 'completed', errorCode: undefined }));
|
|
746
|
+
return this.snapshot(this.options.store.update(id, { state: 'unknown', executionState: 'unknown', errorCode: 'standard_task_recovery_required' }));
|
|
747
|
+
}
|
|
624
748
|
return await this.replay(original);
|
|
625
749
|
}
|
|
626
750
|
catch (error) {
|
package/dist/commerce/cli.js
CHANGED
|
@@ -19,12 +19,14 @@ import { CredentialVault } from './vault.js';
|
|
|
19
19
|
import { X402Gate } from './x402.js';
|
|
20
20
|
import { evmReceiptVerifier, evmSettlementRecovery } from './chain.js';
|
|
21
21
|
import { CommerceError } from './types.js';
|
|
22
|
+
import { inspectLedger } from './operations.js';
|
|
22
23
|
const { values, positionals } = parseArgs({ allowPositionals: true, options: {
|
|
23
24
|
config: { type: 'string' }, service: { type: 'string' }, offer: { type: 'string' }, origin: { type: 'string' },
|
|
24
25
|
version: { type: 'boolean' }, help: { type: 'boolean', short: 'h' }, state: { type: 'string' }, credentials: { type: 'string' }, host: { type: 'string' },
|
|
25
26
|
port: { type: 'string' }, directory: { type: 'string' }, 'envar-config': { type: 'string' }, 'envar-credentials': { type: 'string' },
|
|
27
|
+
after: { type: 'string' }, limit: { type: 'string' },
|
|
26
28
|
} });
|
|
27
|
-
const usage = 'envarpay init --directory DIR | validate --config seller.json | card --config seller.json --service ID --offer ID --origin URL | serve/buyer-serve --config FILE --credentials FILE --state DB --origin URL';
|
|
29
|
+
const usage = 'envarpay init --directory DIR | inspect --state DB [--after ROW --limit 100] | validate --config seller.json | card --config seller.json --service ID --offer ID --origin URL | serve/buyer-serve --config FILE --credentials FILE --state DB --origin URL';
|
|
28
30
|
const closers = [];
|
|
29
31
|
function privateFile(path) {
|
|
30
32
|
const stat = lstatSync(path);
|
|
@@ -84,6 +86,10 @@ async function run() {
|
|
|
84
86
|
return;
|
|
85
87
|
}
|
|
86
88
|
const command = positionals[0];
|
|
89
|
+
if (command === 'inspect' && values.state) {
|
|
90
|
+
console.log(JSON.stringify(inspectLedger(values.state, Number(values.after ?? 0), Number(values.limit ?? 100)), null, 2));
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
87
93
|
if ((values['envar-config'] || values['envar-credentials']) && command !== 'serve')
|
|
88
94
|
throw new CommerceError('envar_role', 'Configuration synchronization is available on seller serve only');
|
|
89
95
|
if (Boolean(values['envar-config']) !== Boolean(values['envar-credentials']))
|
package/dist/commerce/config.js
CHANGED
|
@@ -143,6 +143,24 @@ export function loadBuyerPolicy(value) {
|
|
|
143
143
|
const ids = new Set();
|
|
144
144
|
for (const peer of policy.peers) {
|
|
145
145
|
validateUrl(peer.cardUrl);
|
|
146
|
+
if (peer.mode === 'standard-a2a') {
|
|
147
|
+
const endpoint = validateUrl(peer.endpoint), card = validateUrl(peer.cardUrl);
|
|
148
|
+
if (endpoint.origin !== card.origin || endpoint.search || endpoint.href !== peer.endpoint || card.href !== peer.cardUrl)
|
|
149
|
+
throw new CommerceError('standard_peer_endpoint', 'Review an exact same-origin HTTPS A2A endpoint without query parameters');
|
|
150
|
+
noExternalRefs(peer.localContract.inputSchema);
|
|
151
|
+
if (JSON.stringify(peer.localContract.inputSchema).length > 512 * 1024)
|
|
152
|
+
throw new CommerceError('standard_peer_schema', 'Local input schema exceeds 512 KiB');
|
|
153
|
+
try {
|
|
154
|
+
const inputAjv = new Ajv2020({ strict: true });
|
|
155
|
+
addFormats(inputAjv);
|
|
156
|
+
inputAjv.compile(peer.localContract.inputSchema);
|
|
157
|
+
}
|
|
158
|
+
catch {
|
|
159
|
+
throw new CommerceError('standard_peer_schema', 'Local input schema must be a valid supported JSON Schema');
|
|
160
|
+
}
|
|
161
|
+
if (atomic(peer.localContract.amount) > atomic(peer.maxPerPurchase))
|
|
162
|
+
throw new CommerceError('purchase_limit', 'Locally reviewed standard-peer price exceeds the per-purchase limit');
|
|
163
|
+
}
|
|
146
164
|
if (ids.has(peer.id))
|
|
147
165
|
throw new CommerceError('duplicate_peer', 'Peer IDs must be unique');
|
|
148
166
|
ids.add(peer.id);
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** Local owner-only diagnostics: never opens a signer, vault, provider or network. */
|
|
2
|
+
export declare function inspectLedger(path: string, after?: number, limit?: number): {
|
|
3
|
+
role: string;
|
|
4
|
+
total: number;
|
|
5
|
+
attentionCount: number;
|
|
6
|
+
items: {
|
|
7
|
+
cursor: number;
|
|
8
|
+
id: string;
|
|
9
|
+
paymentState: string;
|
|
10
|
+
executionState: string;
|
|
11
|
+
taskId: string | null;
|
|
12
|
+
}[];
|
|
13
|
+
nextCursor: number;
|
|
14
|
+
hasMore: boolean;
|
|
15
|
+
paymentPerformed: boolean;
|
|
16
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { DatabaseSync } from 'node:sqlite';
|
|
2
|
+
import { lstatSync } from 'node:fs';
|
|
3
|
+
import { CommerceError } from './types.js';
|
|
4
|
+
/** Local owner-only diagnostics: never opens a signer, vault, provider or network. */
|
|
5
|
+
export function inspectLedger(path, after = 0, limit = 100) {
|
|
6
|
+
const stat = lstatSync(path);
|
|
7
|
+
if (!stat.isFile() || stat.isSymbolicLink() || (stat.mode & 0o077) !== 0 || (process.getuid && stat.uid !== process.getuid()))
|
|
8
|
+
throw new CommerceError('private_file', 'Inspect an existing ledger owned only by the current user');
|
|
9
|
+
if (!Number.isSafeInteger(after) || after < 0 || !Number.isSafeInteger(limit) || limit < 1 || limit > 100)
|
|
10
|
+
throw new CommerceError('inspection_cursor', 'Use a nonnegative row cursor and a limit from1 to100');
|
|
11
|
+
const db = new DatabaseSync(path, { readOnly: true });
|
|
12
|
+
try {
|
|
13
|
+
db.exec('PRAGMA query_only=ON; PRAGMA busy_timeout=1000');
|
|
14
|
+
const names = new Set(db.prepare("SELECT name FROM sqlite_master WHERE type='table'").all().map(row => row.name));
|
|
15
|
+
const role = names.has('buyer_purchases') ? 'buyer' : names.has('commerce_orders') ? 'seller' : null;
|
|
16
|
+
if (!role)
|
|
17
|
+
throw new CommerceError('store_version', 'Unrecognized commerce ledger');
|
|
18
|
+
const meta = role === 'buyer' ? 'buyer_meta' : 'commerce_meta', version = role === 'buyer' ? 1 : 2;
|
|
19
|
+
if (db.prepare(`SELECT version FROM ${meta}`).get()?.version !== version)
|
|
20
|
+
throw new CommerceError('store_version', 'Unsupported commerce ledger version');
|
|
21
|
+
const table = role === 'buyer' ? 'buyer_purchases' : 'commerce_orders';
|
|
22
|
+
const total = Number(db.prepare(`SELECT COUNT(*) AS n FROM ${table}`).get().n);
|
|
23
|
+
const filter = role === 'buyer' ? "json_extract(record_json,'$.state') IN ('signing','submitted','unknown') OR json_extract(record_json,'$.paymentState') IN ('reserved','unknown') OR json_extract(record_json,'$.executionState')='unknown'" : "payment_state IN ('settling','unknown') OR execution_state IN ('dispatching','unknown')";
|
|
24
|
+
const selected = role === 'buyer' ? "rowid AS cursor,id,json_extract(record_json,'$.paymentState') AS payment_state,json_extract(record_json,'$.executionState') AS execution_state,json_extract(record_json,'$.task.id') AS task_id" : "rowid AS cursor,id,payment_state,execution_state,task_id";
|
|
25
|
+
const rows = db.prepare(`SELECT ${selected} FROM ${table} WHERE rowid>? AND (${filter}) ORDER BY rowid LIMIT ?`).all(after, limit + 1);
|
|
26
|
+
const page = rows.slice(0, limit).map(row => ({ cursor: Number(row.cursor), id: String(row.id), paymentState: String(row.payment_state), executionState: String(row.execution_state), taskId: typeof row.task_id === 'string' ? row.task_id : null }));
|
|
27
|
+
const attentionCount = Number(db.prepare(`SELECT COUNT(*) AS n FROM ${table} WHERE ${filter}`).get().n);
|
|
28
|
+
return { role, total, attentionCount, items: page, nextCursor: page.at(-1)?.cursor ?? after, hasMore: rows.length > limit, paymentPerformed: false };
|
|
29
|
+
}
|
|
30
|
+
finally {
|
|
31
|
+
db.close();
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -230,7 +230,7 @@ export class A2APeerProxy {
|
|
|
230
230
|
let raw = snapshot?.task
|
|
231
231
|
? structuredClone(snapshot.task)
|
|
232
232
|
: {};
|
|
233
|
-
if (!code && ["failed", "unknown"].includes(String(row.phase))) {
|
|
233
|
+
if (!code && ["created", "attention", "failed", "unknown"].includes(String(row.phase))) {
|
|
234
234
|
try {
|
|
235
235
|
const previous = JSON.parse(String(row.task_json));
|
|
236
236
|
if (typeof previous.metadata?.errorCode === "string")
|
|
@@ -251,9 +251,11 @@ export class A2APeerProxy {
|
|
|
251
251
|
status: {
|
|
252
252
|
state: needsApproval
|
|
253
253
|
? "TASK_STATE_AUTH_REQUIRED"
|
|
254
|
-
:
|
|
255
|
-
? "
|
|
256
|
-
:
|
|
254
|
+
: phase === "attention"
|
|
255
|
+
? "TASK_STATE_INPUT_REQUIRED"
|
|
256
|
+
: failed
|
|
257
|
+
? "TASK_STATE_FAILED"
|
|
258
|
+
: "TASK_STATE_WORKING",
|
|
257
259
|
},
|
|
258
260
|
};
|
|
259
261
|
}
|
|
@@ -458,9 +460,13 @@ export class A2APeerProxy {
|
|
|
458
460
|
"peer_not_allowed",
|
|
459
461
|
"invalid_input",
|
|
460
462
|
"purchase_limit",
|
|
463
|
+
"paid_quote_required",
|
|
464
|
+
"offer_metadata_required",
|
|
465
|
+
"quote_mismatch",
|
|
466
|
+
"retired_revision",
|
|
461
467
|
].includes(error.code);
|
|
462
468
|
this.phase(id, definitelyBlocked
|
|
463
|
-
? "failed"
|
|
469
|
+
? state === "previewing" ? "attention" : "failed"
|
|
464
470
|
: state === "confirming"
|
|
465
471
|
? "unknown"
|
|
466
472
|
: state === "previewing"
|
package/dist/commerce/server.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { timingSafeEqual } from 'node:crypto';
|
|
2
2
|
import { AgentCard, Role, Task, TaskState, SendMessageRequest } from '@a2a-js/sdk';
|
|
3
3
|
import { DefaultRequestHandler, JsonRpcTransportHandler, ServerCallContext } from '@a2a-js/sdk/server';
|
|
4
|
-
import { RequestMalformedError, TaskNotFoundError, UnsupportedOperationError } from '@a2a-js/sdk/errors';
|
|
4
|
+
import { ContentTypeNotSupportedError, RequestMalformedError, TaskNotFoundError, UnsupportedOperationError, VersionNotSupportedError } from '@a2a-js/sdk/errors';
|
|
5
5
|
import { Ajv2020 } from 'ajv/dist/2020.js';
|
|
6
6
|
import addFormatsImport from 'ajv-formats';
|
|
7
7
|
import { createOfferCard, offerPath } from './card.js';
|
|
@@ -171,6 +171,7 @@ export class CommerceServer {
|
|
|
171
171
|
running = false;
|
|
172
172
|
kickPending = false;
|
|
173
173
|
stopped = false;
|
|
174
|
+
workerFailed = false;
|
|
174
175
|
waiters = new Map();
|
|
175
176
|
activeOrders = new Set();
|
|
176
177
|
recoveries = new Map();
|
|
@@ -233,8 +234,19 @@ export class CommerceServer {
|
|
|
233
234
|
const url = new URL(request.url);
|
|
234
235
|
if (url.origin !== new URL(this.options.origin).origin)
|
|
235
236
|
return Response.json({ error: 'invalid_host' }, { status: 421 });
|
|
236
|
-
if (request.method === 'GET' && ['/healthz', '/readyz'].includes(url.pathname))
|
|
237
|
-
|
|
237
|
+
if (request.method === 'GET' && ['/healthz', '/readyz'].includes(url.pathname)) {
|
|
238
|
+
let ready = !this.stopped;
|
|
239
|
+
if (url.pathname === '/readyz') {
|
|
240
|
+
ready = ready && !this.workerFailed;
|
|
241
|
+
try {
|
|
242
|
+
this.store.assertReady();
|
|
243
|
+
}
|
|
244
|
+
catch {
|
|
245
|
+
ready = false;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
return Response.json({ status: ready ? 'ready' : this.stopped ? 'draining' : 'unavailable' }, { status: ready ? 200 : 503, headers: { 'Cache-Control': 'no-store' } });
|
|
249
|
+
}
|
|
238
250
|
const proof = /^\/\.well-known\/envar\/([0-9a-f-]{36})$/.exec(url.pathname);
|
|
239
251
|
if (request.method === 'GET' && proof) {
|
|
240
252
|
const challenge = this.options.ownershipChallenges?.[proof[1]];
|
|
@@ -255,8 +267,6 @@ export class CommerceServer {
|
|
|
255
267
|
const caller = await this.options.authenticate(request);
|
|
256
268
|
if (!caller)
|
|
257
269
|
return Response.json({ error: 'authentication_required' }, { status: 401, headers: { 'WWW-Authenticate': 'Bearer', 'Cache-Control': 'no-store' } });
|
|
258
|
-
if (request.headers.get('A2A-Version') !== '1.0')
|
|
259
|
-
return Response.json({ error: 'A2A-Version 1.0 required' }, { status: 400 });
|
|
260
270
|
if (Number(request.headers.get('Content-Length') ?? 0) > 1024 * 1024)
|
|
261
271
|
return Response.json({ error: 'request_too_large' }, { status: 413 });
|
|
262
272
|
let size = 0;
|
|
@@ -283,6 +293,14 @@ export class CommerceServer {
|
|
|
283
293
|
throw new RequestMalformedError('A single JSONRPC object is required');
|
|
284
294
|
requestId = raw.id ?? null;
|
|
285
295
|
const params = raw.params;
|
|
296
|
+
// Parse only the bounded envelope to preserve its request ID. Reject these
|
|
297
|
+
// transport conditions before any quote, payment gate or Task dispatch.
|
|
298
|
+
if (request.headers.get('A2A-Version') !== '1.0')
|
|
299
|
+
throw new VersionNotSupportedError('This service supports A2A-Version 1.0');
|
|
300
|
+
if (request.headers.get('Content-Type')?.split(';', 1)[0]?.trim().toLowerCase() !== 'application/json')
|
|
301
|
+
throw new ContentTypeNotSupportedError('Send application/json to this JSON-RPC endpoint');
|
|
302
|
+
if (raw.method === 'SendMessage' && this.workerFailed)
|
|
303
|
+
return Response.json({ error: 'worker_unavailable' }, { status: 503, headers: { 'Cache-Control': 'no-store' } });
|
|
286
304
|
if (params && typeof params === 'object' && 'tenant' in params && params.tenant !== route.tenant)
|
|
287
305
|
return Response.json({ jsonrpc: '2.0', id: raw.id ?? null, error: { code: -32602, message: 'Invalid tenant' } }, { status: 200 });
|
|
288
306
|
let paymentHeaders = {};
|
|
@@ -315,7 +333,7 @@ export class CommerceServer {
|
|
|
315
333
|
}
|
|
316
334
|
catch (error) {
|
|
317
335
|
const mapped = error instanceof CommerceError ? { code: -32000, message: error.message, data: { code: error.code } } : JsonRpcTransportHandler.mapToJSONRPCError(error);
|
|
318
|
-
return Response.json({ jsonrpc: '2.0', id: requestId, error: mapped }, { headers: { 'Cache-Control': 'no-store' } });
|
|
336
|
+
return Response.json({ jsonrpc: '2.0', id: requestId, error: mapped }, { headers: { 'A2A-Version': '1.0', 'Cache-Control': 'no-store' } });
|
|
319
337
|
}
|
|
320
338
|
}
|
|
321
339
|
async continueTask(params, input, service, offerId, context) {
|
|
@@ -393,14 +411,14 @@ export class CommerceServer {
|
|
|
393
411
|
return projected;
|
|
394
412
|
}
|
|
395
413
|
schedule() {
|
|
396
|
-
if (this.stopped)
|
|
414
|
+
if (this.stopped || this.workerFailed)
|
|
397
415
|
return;
|
|
398
416
|
if (this.running) {
|
|
399
417
|
this.kickPending = true;
|
|
400
418
|
return;
|
|
401
419
|
}
|
|
402
420
|
this.running = true;
|
|
403
|
-
void this.drain().catch(() => this.options.onError?.('worker_failure')).finally(() => { this.running = false; if (this.kickPending) {
|
|
421
|
+
void this.drain().catch(() => { this.workerFailed = true; this.options.onError?.('worker_failure'); }).finally(() => { this.running = false; if (this.kickPending) {
|
|
404
422
|
this.kickPending = false;
|
|
405
423
|
this.schedule();
|
|
406
424
|
} });
|
package/dist/commerce/store.d.ts
CHANGED
|
@@ -36,6 +36,8 @@ export declare class CommerceStore implements TaskStore {
|
|
|
36
36
|
constructor(path: string);
|
|
37
37
|
private migrateV1;
|
|
38
38
|
close(): void;
|
|
39
|
+
/** Local readiness only. External settlement is independently verified per purchase. */
|
|
40
|
+
assertReady(): void;
|
|
39
41
|
registerCatalog(config: CommerceConfig): void;
|
|
40
42
|
serviceRevision(id: string): Service | undefined;
|
|
41
43
|
catalogHistory(): {
|
package/dist/commerce/store.js
CHANGED
|
@@ -134,6 +134,13 @@ export class CommerceStore {
|
|
|
134
134
|
this.ownerDb.close();
|
|
135
135
|
}
|
|
136
136
|
}
|
|
137
|
+
/** Local readiness only. External settlement is independently verified per purchase. */
|
|
138
|
+
assertReady() {
|
|
139
|
+
const row = this.db.prepare('SELECT version FROM commerce_meta').get();
|
|
140
|
+
if (row?.version !== 2)
|
|
141
|
+
throw new CommerceError('store_unavailable', 'Commerce ledger is unavailable');
|
|
142
|
+
this.db.prepare('SELECT id FROM commerce_orders LIMIT 1').get();
|
|
143
|
+
}
|
|
137
144
|
registerCatalog(config) {
|
|
138
145
|
this.transaction(() => {
|
|
139
146
|
for (const service of config.services) {
|
package/dist/commerce/types.d.ts
CHANGED
|
@@ -65,13 +65,33 @@ export interface CommerceConfig {
|
|
|
65
65
|
paymentProfiles: Record<string, PaymentProfile>;
|
|
66
66
|
services: Service[];
|
|
67
67
|
}
|
|
68
|
+
/** A separately reviewed transport profile; local terms are never advertised as seller promises. */
|
|
69
|
+
export interface StandardA2APeer {
|
|
70
|
+
id: string;
|
|
71
|
+
cardUrl: string;
|
|
72
|
+
mode: 'standard-a2a';
|
|
73
|
+
protocol: 'x402';
|
|
74
|
+
currency: string;
|
|
75
|
+
recipient: string;
|
|
76
|
+
maxPerPurchase: string;
|
|
77
|
+
endpoint: string;
|
|
78
|
+
authentication: 'none' | 'bearer';
|
|
79
|
+
localContract: {
|
|
80
|
+
revision: number;
|
|
81
|
+
offerId: string;
|
|
82
|
+
inputSchema: Record<string, unknown>;
|
|
83
|
+
amount: string;
|
|
84
|
+
paidOnly: true;
|
|
85
|
+
};
|
|
86
|
+
}
|
|
68
87
|
export interface BuyerPolicy {
|
|
69
88
|
policyVersion: 1;
|
|
70
89
|
paymentsEnabled: boolean;
|
|
71
90
|
approval: 'per_purchase' | 'within_preapproved_limits';
|
|
72
|
-
peers: ({
|
|
91
|
+
peers: (StandardA2APeer | {
|
|
73
92
|
id: string;
|
|
74
93
|
cardUrl: string;
|
|
94
|
+
mode?: 'envarpay';
|
|
75
95
|
protocol: 'x402' | 'mpp';
|
|
76
96
|
currency: string;
|
|
77
97
|
recipient: string;
|
|
@@ -79,6 +99,7 @@ export interface BuyerPolicy {
|
|
|
79
99
|
} | {
|
|
80
100
|
id: string;
|
|
81
101
|
cardUrl: string;
|
|
102
|
+
mode?: 'envarpay';
|
|
82
103
|
protocol: 'free';
|
|
83
104
|
currency: null;
|
|
84
105
|
recipient: null;
|
package/package.json
CHANGED
|
@@ -70,6 +70,54 @@
|
|
|
70
70
|
"maxPerPurchase": {
|
|
71
71
|
"type": "string",
|
|
72
72
|
"pattern": "^(0|[1-9][0-9]{0,77})$"
|
|
73
|
+
},
|
|
74
|
+
"mode": {
|
|
75
|
+
"enum": [
|
|
76
|
+
"envarpay",
|
|
77
|
+
"standard-a2a"
|
|
78
|
+
]
|
|
79
|
+
},
|
|
80
|
+
"endpoint": {
|
|
81
|
+
"type": "string",
|
|
82
|
+
"format": "uri",
|
|
83
|
+
"pattern": "^https://"
|
|
84
|
+
},
|
|
85
|
+
"authentication": {
|
|
86
|
+
"enum": [
|
|
87
|
+
"none",
|
|
88
|
+
"bearer"
|
|
89
|
+
]
|
|
90
|
+
},
|
|
91
|
+
"localContract": {
|
|
92
|
+
"type": "object",
|
|
93
|
+
"additionalProperties": false,
|
|
94
|
+
"required": [
|
|
95
|
+
"revision",
|
|
96
|
+
"offerId",
|
|
97
|
+
"inputSchema",
|
|
98
|
+
"amount",
|
|
99
|
+
"paidOnly"
|
|
100
|
+
],
|
|
101
|
+
"properties": {
|
|
102
|
+
"revision": {
|
|
103
|
+
"type": "integer",
|
|
104
|
+
"minimum": 1,
|
|
105
|
+
"maximum": 9007199254740991
|
|
106
|
+
},
|
|
107
|
+
"offerId": {
|
|
108
|
+
"type": "string",
|
|
109
|
+
"pattern": "^[a-z][a-z0-9-]{0,63}$"
|
|
110
|
+
},
|
|
111
|
+
"inputSchema": {
|
|
112
|
+
"type": "object"
|
|
113
|
+
},
|
|
114
|
+
"amount": {
|
|
115
|
+
"$ref": "#/$defs/amount"
|
|
116
|
+
},
|
|
117
|
+
"paidOnly": {
|
|
118
|
+
"const": true
|
|
119
|
+
}
|
|
120
|
+
}
|
|
73
121
|
}
|
|
74
122
|
},
|
|
75
123
|
"allOf": [
|
|
@@ -109,6 +157,63 @@
|
|
|
109
157
|
}
|
|
110
158
|
}
|
|
111
159
|
}
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
"if": {
|
|
163
|
+
"required": [
|
|
164
|
+
"mode"
|
|
165
|
+
],
|
|
166
|
+
"properties": {
|
|
167
|
+
"mode": {
|
|
168
|
+
"const": "standard-a2a"
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
},
|
|
172
|
+
"then": {
|
|
173
|
+
"required": [
|
|
174
|
+
"endpoint",
|
|
175
|
+
"authentication",
|
|
176
|
+
"localContract"
|
|
177
|
+
],
|
|
178
|
+
"properties": {
|
|
179
|
+
"protocol": {
|
|
180
|
+
"const": "x402"
|
|
181
|
+
},
|
|
182
|
+
"endpoint": {},
|
|
183
|
+
"authentication": {},
|
|
184
|
+
"localContract": {}
|
|
185
|
+
}
|
|
186
|
+
},
|
|
187
|
+
"else": {
|
|
188
|
+
"not": {
|
|
189
|
+
"anyOf": [
|
|
190
|
+
{
|
|
191
|
+
"required": [
|
|
192
|
+
"endpoint"
|
|
193
|
+
],
|
|
194
|
+
"properties": {
|
|
195
|
+
"endpoint": {}
|
|
196
|
+
}
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
"required": [
|
|
200
|
+
"authentication"
|
|
201
|
+
],
|
|
202
|
+
"properties": {
|
|
203
|
+
"authentication": {}
|
|
204
|
+
}
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
"required": [
|
|
208
|
+
"localContract"
|
|
209
|
+
],
|
|
210
|
+
"properties": {
|
|
211
|
+
"localContract": {}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
]
|
|
215
|
+
}
|
|
216
|
+
}
|
|
112
217
|
}
|
|
113
218
|
]
|
|
114
219
|
}
|