@envarai/envarpay 0.1.0-alpha.2 → 0.2.0-alpha.1

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.
Files changed (59) hide show
  1. package/README.md +56 -145
  2. package/dist/commerce/buyer-store.d.ts +93 -0
  3. package/dist/commerce/buyer-store.js +168 -0
  4. package/dist/commerce/buyer.d.ts +129 -0
  5. package/dist/commerce/buyer.js +631 -0
  6. package/dist/commerce/card.d.ts +5 -0
  7. package/dist/commerce/card.js +29 -0
  8. package/dist/commerce/chain.d.ts +24 -0
  9. package/dist/commerce/chain.js +146 -0
  10. package/dist/commerce/cli.d.ts +2 -0
  11. package/dist/commerce/cli.js +268 -0
  12. package/dist/commerce/client-runtime.d.ts +8 -0
  13. package/dist/commerce/client-runtime.js +7 -0
  14. package/dist/commerce/config.d.ts +7 -0
  15. package/dist/commerce/config.js +164 -0
  16. package/dist/commerce/envar-runtime.d.ts +46 -0
  17. package/dist/commerce/envar-runtime.js +256 -0
  18. package/dist/commerce/envar.d.ts +126 -0
  19. package/dist/commerce/envar.js +647 -0
  20. package/dist/commerce/http.d.ts +2 -0
  21. package/dist/commerce/http.js +34 -0
  22. package/dist/commerce/index.d.ts +4 -0
  23. package/dist/commerce/index.js +4 -0
  24. package/dist/commerce/management.d.ts +14 -0
  25. package/dist/commerce/management.js +138 -0
  26. package/dist/commerce/mpp-client.d.ts +59 -0
  27. package/dist/commerce/mpp-client.js +24 -0
  28. package/dist/commerce/mpp-runtime.d.ts +3 -0
  29. package/dist/commerce/mpp-runtime.js +3 -0
  30. package/dist/commerce/mpp.d.ts +53 -0
  31. package/dist/commerce/mpp.js +163 -0
  32. package/dist/commerce/peer-proxy.d.ts +44 -0
  33. package/dist/commerce/peer-proxy.js +667 -0
  34. package/dist/commerce/pricing.d.ts +12 -0
  35. package/dist/commerce/pricing.js +60 -0
  36. package/dist/commerce/runtime.d.ts +8 -0
  37. package/dist/commerce/runtime.js +8 -0
  38. package/dist/commerce/server.d.ts +58 -0
  39. package/dist/commerce/server.js +485 -0
  40. package/dist/commerce/store.d.ts +86 -0
  41. package/dist/commerce/store.js +466 -0
  42. package/dist/commerce/stripe-provider.d.ts +9 -0
  43. package/dist/commerce/stripe-provider.js +97 -0
  44. package/dist/commerce/types.d.ts +113 -0
  45. package/dist/commerce/types.js +8 -0
  46. package/dist/commerce/upstream.d.ts +6 -0
  47. package/dist/commerce/upstream.js +103 -0
  48. package/dist/commerce/vault.d.ts +8 -0
  49. package/dist/commerce/vault.js +55 -0
  50. package/dist/commerce/x402.d.ts +37 -0
  51. package/dist/commerce/x402.js +174 -0
  52. package/dist/index.js +3 -1
  53. package/examples/buyer-policy.json +18 -0
  54. package/examples/envar-integration-policy.json +18 -0
  55. package/examples/seller.json +52 -0
  56. package/package.json +75 -10
  57. package/schemas/buyer-policy.schema.json +148 -0
  58. package/schemas/envarpay.config.schema.json +376 -0
  59. package/schemas/quote.schema.json +91 -0
@@ -0,0 +1,5 @@
1
+ import { AgentCard } from '@a2a-js/sdk';
2
+ import type { CommerceConfig } from './types.js';
3
+ /** One deterministic offer route, serialized by the official A2A 1.0 codec. */
4
+ export declare function offerPath(serviceId: string, revision: number, offerId: string): string;
5
+ export declare function createOfferCard(config: CommerceConfig, serviceId: string, offerId: string, origin: string): AgentCard;
@@ -0,0 +1,29 @@
1
+ import { AgentCard } from '@a2a-js/sdk';
2
+ import { CommerceError } from './types.js';
3
+ import { validateUrl, digest } from './config.js';
4
+ /** One deterministic offer route, serialized by the official A2A 1.0 codec. */
5
+ export function offerPath(serviceId, revision, offerId) {
6
+ return `/services/${encodeURIComponent(serviceId)}/v${revision}/offers/${encodeURIComponent(offerId)}`;
7
+ }
8
+ export function createOfferCard(config, serviceId, offerId, origin) {
9
+ const base = validateUrl(origin, true);
10
+ if (base.search || base.pathname !== '/')
11
+ throw new CommerceError('invalid_origin', 'Origin must not contain a path or query');
12
+ const service = config.services.find(x => x.id === serviceId);
13
+ const offer = service?.offers.find(x => x.id === offerId);
14
+ if (!service || !offer)
15
+ throw new CommerceError('offer_not_found', 'Service offer not found');
16
+ const profile = offer.paymentProfile ? config.paymentProfiles[offer.paymentProfile] : null;
17
+ const payment = profile?.adapter === 'x402' ? { adapter: 'x402', network: profile.network, asset: profile.asset, payTo: profile.payTo } : profile ? { adapter: 'mpp', method: 'stripe', intent: 'charge', currency: profile.currency } : null;
18
+ return AgentCard.fromJSON({
19
+ name: service.name,
20
+ description: `Deliverables: ${service.contract.deliverables.join(', ')}`,
21
+ version: String(service.revision),
22
+ supportedInterfaces: [{ url: new URL(`${offerPath(service.id, service.revision, offer.id)}/a2a`, base).href, protocolBinding: 'JSONRPC', protocolVersion: '1.0' }],
23
+ capabilities: { streaming: false, pushNotifications: false, extensions: [{ uri: 'urn:envarpay:commerce:1', required: false, description: 'Optional service offer metadata; standard A2A and payment transports remain independently usable', params: { serviceId: service.id, serviceRevision: service.revision, offerId: offer.id, pricing: offer.pricing, collection: offer.collection, contract: service.contract, payment, termsDigest: digest({ contract: service.contract, offer, profile }) } }] },
24
+ defaultInputModes: ['application/json'], defaultOutputModes: ['application/json', 'text/plain'],
25
+ skills: [{ id: service.id, name: service.name, description: `Purchase ${service.name}`, tags: [service.id] }],
26
+ securitySchemes: { bearer: { httpAuthSecurityScheme: { scheme: 'bearer' } } },
27
+ securityRequirements: [{ schemes: { bearer: { list: [] } } }],
28
+ });
29
+ }
@@ -0,0 +1,24 @@
1
+ import type { PaymentPayload, PaymentRequirements, SettleResponse } from '@x402/core/types';
2
+ export interface ExpiredUnusedProof {
3
+ expiredUnused: true;
4
+ network: string;
5
+ asset: string;
6
+ payer: string;
7
+ nonce: string;
8
+ validAfter: string;
9
+ validBefore: string;
10
+ blockNumber: string;
11
+ blockHash: string;
12
+ blockTimestamp: string;
13
+ finality: 'finalized';
14
+ authorizationUsed: false;
15
+ }
16
+ /** Checks the canonical chain receipt and exact official-token Transfer. */
17
+ export declare function evmReceiptVerifier(rpcUrls: Readonly<Record<string, string>>, confirmations?: number): (receipt: SettleResponse, payload: PaymentPayload, requirements: PaymentRequirements) => Promise<boolean>;
18
+ /** Locate an original EIP-3009 settlement even when facilitator lost its tx response. */
19
+ export declare function evmSettlementRecovery(rpcUrls: Readonly<Record<string, string>>): {
20
+ expiredUnusedProof: (payload: PaymentPayload, requirements: PaymentRequirements) => Promise<ExpiredUnusedProof | undefined>;
21
+ proveExpiredUnused: (payload: PaymentPayload, requirements: PaymentRequirements) => Promise<boolean>;
22
+ checkpoint: (requirements: PaymentRequirements) => Promise<string>;
23
+ findOriginalReceipt: (payload: PaymentPayload, requirements: PaymentRequirements, fromBlock: string) => Promise<SettleResponse | undefined>;
24
+ };
@@ -0,0 +1,146 @@
1
+ import { createPublicClient, http, decodeEventLog, parseAbi } from 'viem';
2
+ import { CommerceError } from './types.js';
3
+ import { validateUrl } from './config.js';
4
+ const abi = parseAbi(['event Transfer(address indexed from, address indexed to, uint256 value)']);
5
+ const authorizationAbi = parseAbi(['event AuthorizationUsed(address indexed authorizer, bytes32 indexed nonce)']);
6
+ const authorizationStateAbi = parseAbi(['function authorizationState(address authorizer,bytes32 nonce) view returns (bool)']);
7
+ const officialUsdc = {
8
+ 'eip155:84532': '0x036cbd53842c5426634e7929541ec2318f3dcf7e',
9
+ 'eip155:8453': '0x833589fcd6edb6e08f4c7c32d4f71b54bda02913',
10
+ };
11
+ /** Checks the canonical chain receipt and exact official-token Transfer. */
12
+ export function evmReceiptVerifier(rpcUrls, confirmations = 2) {
13
+ if (!Number.isInteger(confirmations) || confirmations < 1)
14
+ throw new CommerceError('confirmation_policy', 'Positive confirmation depth required');
15
+ return async (receipt, payload, requirements) => {
16
+ const rpc = rpcUrls[requirements.network];
17
+ if (!rpc)
18
+ throw new CommerceError('rpc_missing', 'Configure the settlement network RPC');
19
+ validateUrl(rpc, true);
20
+ const chainId = Number(requirements.network.split(':')[1]);
21
+ if (!Number.isSafeInteger(chainId) || !/^0x[0-9a-fA-F]{64}$/.test(receipt.transaction) || receipt.network !== requirements.network)
22
+ return false;
23
+ const client = createPublicClient({ transport: http(rpc, { timeout: 15000, retryCount: 0 }) });
24
+ if (await client.getChainId() !== chainId)
25
+ return false;
26
+ const mined = await client.getTransactionReceipt({ hash: receipt.transaction });
27
+ if (mined.status !== 'success')
28
+ return false;
29
+ const block = await client.getBlock({ blockNumber: mined.blockNumber });
30
+ if (block.hash !== mined.blockHash)
31
+ return false;
32
+ const latest = await client.getBlockNumber();
33
+ if (latest - mined.blockNumber + 1n < BigInt(confirmations))
34
+ return false;
35
+ const auth = payload.payload.authorization;
36
+ if (!auth?.from || !auth.nonce || auth.to?.toLowerCase() !== requirements.payTo.toLowerCase() || auth.value !== requirements.amount)
37
+ return false;
38
+ if (receipt.payer && receipt.payer.toLowerCase() !== auth.from.toLowerCase())
39
+ return false;
40
+ const matches = mined.logs.filter(log => {
41
+ if (log.address.toLowerCase() !== requirements.asset.toLowerCase())
42
+ return false;
43
+ try {
44
+ const event = decodeEventLog({ abi, data: log.data, topics: log.topics });
45
+ return event.args.from.toLowerCase() === auth.from.toLowerCase() && event.args.to.toLowerCase() === requirements.payTo.toLowerCase() && event.args.value === BigInt(requirements.amount);
46
+ }
47
+ catch {
48
+ return false;
49
+ }
50
+ });
51
+ const authorizations = mined.logs.filter(log => {
52
+ if (log.address.toLowerCase() !== requirements.asset.toLowerCase())
53
+ return false;
54
+ try {
55
+ const event = decodeEventLog({ abi: authorizationAbi, data: log.data, topics: log.topics });
56
+ return event.args.authorizer.toLowerCase() === auth.from.toLowerCase() && event.args.nonce.toLowerCase() === auth.nonce.toLowerCase();
57
+ }
58
+ catch {
59
+ return false;
60
+ }
61
+ });
62
+ return matches.length === 1 && authorizations.length === 1;
63
+ };
64
+ }
65
+ /** Locate an original EIP-3009 settlement even when facilitator lost its tx response. */
66
+ export function evmSettlementRecovery(rpcUrls) {
67
+ const clientFor = async (requirements) => {
68
+ const rpc = rpcUrls[requirements.network];
69
+ if (!rpc)
70
+ throw new CommerceError('rpc_missing', 'Configure the settlement network RPC');
71
+ validateUrl(rpc, true);
72
+ const client = createPublicClient({ transport: http(rpc, { timeout: 15000, retryCount: 0 }) });
73
+ if (await client.getChainId() !== Number(requirements.network.split(':')[1]))
74
+ throw new CommerceError('rpc_network', 'RPC network does not match frozen requirements');
75
+ return client;
76
+ };
77
+ const expiredUnusedProof = async (payload, requirements) => {
78
+ try {
79
+ const asset = officialUsdc[requirements.network];
80
+ if (!asset || typeof requirements.asset !== 'string' || requirements.asset.toLowerCase() !== asset)
81
+ return undefined;
82
+ const auth = payload.payload?.authorization;
83
+ if (!auth || !/^0x[0-9a-fA-F]{40}$/.test(auth.from ?? '') || !/^0x[0-9a-fA-F]{64}$/.test(auth.nonce ?? '') ||
84
+ auth.to?.toLowerCase() !== requirements.payTo.toLowerCase() || auth.value !== requirements.amount ||
85
+ !/^(0|[1-9][0-9]{0,77})$/.test(auth.validAfter ?? '') || !/^(0|[1-9][0-9]{0,77})$/.test(auth.validBefore ?? ''))
86
+ return undefined;
87
+ const validAfter = BigInt(auth.validAfter), validBefore = BigInt(auth.validBefore);
88
+ if (validAfter >= validBefore || validBefore >= 2n ** 256n)
89
+ return undefined;
90
+ const client = await clientFor(requirements);
91
+ const finalized = await client.getBlock({ blockTag: 'finalized' });
92
+ if (finalized.number === null || !/^0x[0-9a-fA-F]{64}$/.test(finalized.hash ?? '') ||
93
+ finalized.timestamp < validBefore || finalized.timestamp < validAfter)
94
+ return undefined;
95
+ // A missing nonce log is not proof. Read the official token's state at
96
+ // the exact finalized block, after this authorization can no longer execute.
97
+ const used = await client.readContract({ address: asset, abi: authorizationStateAbi, functionName: 'authorizationState',
98
+ args: [auth.from, auth.nonce], blockNumber: finalized.number });
99
+ if (used !== false)
100
+ return undefined;
101
+ const canonical = await client.getBlock({ blockNumber: finalized.number });
102
+ if (canonical.hash !== finalized.hash || canonical.number !== finalized.number || canonical.timestamp !== finalized.timestamp)
103
+ return undefined;
104
+ return { expiredUnused: true, network: requirements.network, asset, payer: auth.from.toLowerCase(), nonce: auth.nonce.toLowerCase(),
105
+ validAfter: auth.validAfter, validBefore: auth.validBefore, blockNumber: finalized.number.toString(),
106
+ blockHash: finalized.hash, blockTimestamp: finalized.timestamp.toString(), finality: 'finalized', authorizationUsed: false };
107
+ }
108
+ catch {
109
+ // Unsupported finalized tags, archive state gaps, malformed return values
110
+ // and network errors retain the original unknown operation and its budget.
111
+ return undefined;
112
+ }
113
+ };
114
+ return {
115
+ expiredUnusedProof,
116
+ proveExpiredUnused: async (payload, requirements) => !!await expiredUnusedProof(payload, requirements),
117
+ checkpoint: async (requirements) => {
118
+ const client = await clientFor(requirements);
119
+ const block = await client.getBlockNumber();
120
+ return (block > 12n ? block - 12n : 0n).toString();
121
+ },
122
+ findOriginalReceipt: async (payload, requirements, fromBlock) => {
123
+ if (!/^(0|[1-9][0-9]*)$/.test(fromBlock))
124
+ throw new CommerceError('recovery_checkpoint', 'Invalid original chain checkpoint');
125
+ const auth = payload.payload.authorization;
126
+ if (!/^0x[0-9a-fA-F]{40}$/.test(auth?.from ?? '') || !/^0x[0-9a-fA-F]{64}$/.test(auth?.nonce ?? ''))
127
+ throw new CommerceError('invalid_payment', 'Missing original EIP-3009 identity');
128
+ const client = await clientFor(requirements);
129
+ const latest = await client.getBlockNumber();
130
+ // Bounded per reconciliation. Very old operations need an operator/archive
131
+ // RPC lookup; never reinterpret incomplete search as proof of non-payment.
132
+ if (latest - BigInt(fromBlock) > 100000n)
133
+ throw new CommerceError('archive_reconciliation_required', 'Original authorization requires an archive log lookup');
134
+ for (let start = BigInt(fromBlock); start <= latest; start += 2000n) {
135
+ const end = start + 1999n < latest ? start + 1999n : latest;
136
+ const logs = await client.getLogs({ address: requirements.asset, event: authorizationAbi[0], args: { authorizer: auth.from, nonce: auth.nonce }, fromBlock: start, toBlock: end, strict: true });
137
+ const settled = logs.filter(log => !log.removed && log.transactionHash);
138
+ if (settled.length > 1)
139
+ throw new CommerceError('ambiguous_receipt', 'Multiple authorization events require manual reconciliation');
140
+ if (settled.length === 1)
141
+ return { success: true, transaction: settled[0].transactionHash, network: requirements.network, payer: auth.from };
142
+ }
143
+ return undefined;
144
+ },
145
+ };
146
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,268 @@
1
+ #!/usr/bin/env node
2
+ import { readFileSync, lstatSync, mkdirSync, writeFileSync } from 'node:fs';
3
+ import { parseArgs } from 'node:util';
4
+ import { createRequire } from 'node:module';
5
+ import { randomBytes } from 'node:crypto';
6
+ import { once } from 'node:events';
7
+ import { dirname, join, resolve } from 'node:path';
8
+ import { pathToFileURL } from 'node:url';
9
+ import { privateKeyToAccount } from 'viem/accounts';
10
+ import { AgentCard } from '@a2a-js/sdk';
11
+ import { loadCommerceConfig, loadBuyerPolicy, digest } from './config.js';
12
+ import { createOfferCard } from './card.js';
13
+ import { CommerceStore } from './store.js';
14
+ import { CommerceServer, bearerAuthenticator } from './server.js';
15
+ import { EnvarIntegration } from './envar.js';
16
+ import { EnvarSellerRuntime, PinnedUpstreams, restoreEnvarConfig } from './envar-runtime.js';
17
+ import { listenCommerce } from './http.js';
18
+ import { CredentialVault } from './vault.js';
19
+ import { X402Gate } from './x402.js';
20
+ import { evmReceiptVerifier, evmSettlementRecovery } from './chain.js';
21
+ import { CommerceError } from './types.js';
22
+ const { values, positionals } = parseArgs({ allowPositionals: true, options: {
23
+ config: { type: 'string' }, service: { type: 'string' }, offer: { type: 'string' }, origin: { type: 'string' },
24
+ version: { type: 'boolean' }, help: { type: 'boolean', short: 'h' }, state: { type: 'string' }, credentials: { type: 'string' }, host: { type: 'string' },
25
+ port: { type: 'string' }, directory: { type: 'string' }, 'envar-config': { type: 'string' }, 'envar-credentials': { type: 'string' },
26
+ } });
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';
28
+ const closers = [];
29
+ function privateFile(path) {
30
+ const stat = lstatSync(path);
31
+ if (!stat.isFile() || stat.isSymbolicLink() || (stat.mode & 0o077) !== 0 || stat.size > 1024 * 1024)
32
+ throw new CommerceError('private_file', 'Credentials must be bounded owner-only files (chmod 600)');
33
+ return readFileSync(path);
34
+ }
35
+ function port(defaultPort) {
36
+ const result = Number(values.port ?? defaultPort);
37
+ if (!Number.isInteger(result) || result < 1 || result > 65535)
38
+ throw new CommerceError('listen_port', 'Invalid listen port');
39
+ return result;
40
+ }
41
+ function signals() {
42
+ let stopping = false;
43
+ const stop = async () => {
44
+ if (stopping)
45
+ return;
46
+ stopping = true;
47
+ const deadline = setTimeout(() => process.exit(1), 60000);
48
+ deadline.unref();
49
+ let failed = false;
50
+ for (const close of closers.reverse()) {
51
+ try {
52
+ await close();
53
+ }
54
+ catch {
55
+ failed = true;
56
+ }
57
+ }
58
+ clearTimeout(deadline);
59
+ process.exit(failed ? 1 : 0);
60
+ };
61
+ process.once('SIGTERM', () => void stop());
62
+ process.once('SIGINT', () => void stop());
63
+ }
64
+ async function closeHttp(http) {
65
+ await new Promise((resolve, reject) => http.close(error => error ? reject(error) : resolve()));
66
+ }
67
+ async function loadMppAdapter(path) {
68
+ privateFile(path);
69
+ const module = await import(pathToFileURL(resolve(path)).href);
70
+ if (typeof module.createMppBuyerOptions !== 'function')
71
+ throw new CommerceError('mpp_adapter_module', 'Owner-supplied module must export createMppBuyerOptions');
72
+ const options = await module.createMppBuyerOptions();
73
+ if (!options || typeof options.createToken !== 'function' || typeof options.recoverToken !== 'function' || typeof options.verifyReceipt !== 'function')
74
+ throw new CommerceError('mpp_adapter_module', 'MPP requires real token creation, original-operation recovery and receipt verification');
75
+ return options;
76
+ }
77
+ async function run() {
78
+ if (values.help || positionals.length === 0 && !values.version) {
79
+ console.log(usage);
80
+ return;
81
+ }
82
+ if (values.version) {
83
+ console.log(createRequire(import.meta.url)('../../package.json').version);
84
+ return;
85
+ }
86
+ const command = positionals[0];
87
+ if ((values['envar-config'] || values['envar-credentials']) && command !== 'serve')
88
+ throw new CommerceError('envar_role', 'Configuration synchronization is available on seller serve only');
89
+ if (Boolean(values['envar-config']) !== Boolean(values['envar-credentials']))
90
+ throw new CommerceError('envar_configuration', 'Supply both --envar-config and --envar-credentials');
91
+ if (command === 'init' && values.directory) {
92
+ mkdirSync(values.directory, { recursive: true, mode: 0o700 });
93
+ for (const [name, source] of [['seller.json', 'seller.json'], ['buyer-policy.json', 'buyer-policy.json']]) {
94
+ writeFileSync(join(values.directory, name), readFileSync(new URL(`../../examples/${source}`, import.meta.url)), { flag: 'wx', mode: 0o600 });
95
+ }
96
+ writeFileSync(join(values.directory, 'vault.key'), randomBytes(32), { flag: 'wx', mode: 0o600, flush: true });
97
+ console.log(JSON.stringify({ directory: values.directory, configured: false, paymentPerformed: false, next: 'Review placeholder addresses, prices, runtime and private credential files before serve' }));
98
+ return;
99
+ }
100
+ if (!values.config)
101
+ throw new CommerceError('usage', usage);
102
+ const raw = JSON.parse(readFileSync(values.config, 'utf8'));
103
+ if (command === 'buyer-serve') {
104
+ if (!values.origin || !values.state || !values.credentials)
105
+ throw new CommerceError('usage', usage);
106
+ const policy = loadBuyerPolicy(raw);
107
+ const secrets = JSON.parse(privateFile(values.credentials).toString('utf8'));
108
+ const needsEvm = policy.peers.some(peer => peer.protocol === 'x402'), needsMpp = policy.peers.some(peer => peer.protocol === 'mpp');
109
+ if (!secrets.peerTokens || (needsEvm || needsMpp) && !secrets.vaultKeyFile)
110
+ throw new CommerceError('buyer_credentials', 'Paid buyer needs private vault key and peer credentials');
111
+ let signer;
112
+ if (needsEvm) {
113
+ if (!secrets.privateKeyFile || !secrets.rpcUrls)
114
+ throw new CommerceError('buyer_credentials', 'x402 peers require local signer and RPC configuration');
115
+ const key = privateFile(secrets.privateKeyFile).toString('utf8').trim();
116
+ if (!/^0x[0-9a-fA-F]{64}$/.test(key))
117
+ throw new CommerceError('buyer_key', 'Signer file must contain one 32-byte 0x-prefixed private key');
118
+ signer = privateKeyToAccount(key);
119
+ }
120
+ if (needsMpp && !secrets.mppAdapterModule)
121
+ throw new CommerceError('mpp_adapter_module', 'MPP peers require an explicit local adapter module for authorized funding and receipt verification');
122
+ const mpp = needsMpp ? await loadMppAdapter(secrets.mppAdapterModule) : undefined;
123
+ const { BuyerStore } = await import('./buyer-store.js');
124
+ const { CommerceBuyer } = await import('./buyer.js');
125
+ const { BuyerManagement, listenBuyerManagement } = await import('./management.js');
126
+ const store = new BuyerStore(values.state);
127
+ closers.push(() => store.close());
128
+ const buyer = new CommerceBuyer({ policy, store, vault: secrets.vaultKeyFile ? new CredentialVault(join(dirname(values.state), 'buyer-authorizations'), privateFile(secrets.vaultKeyFile)) : undefined,
129
+ signer, mpp, peerTokens: secrets.peerTokens, peerEndpoints: secrets.peerEndpoints,
130
+ ...(needsEvm ? { verifyReceipt: evmReceiptVerifier(secrets.rpcUrls), ...evmSettlementRecovery(secrets.rpcUrls) } : {}) });
131
+ closers.push(() => buyer.stop());
132
+ if (secrets.peerProxies?.length) {
133
+ if (secrets.peerProxies.length > 8)
134
+ throw new CommerceError('proxy_limit', 'Configure at most8 explicit peer proxies per buyer');
135
+ const { A2APeerProxy, listenPeerProxy } = await import('./peer-proxy.js');
136
+ for (const settings of secrets.peerProxies) {
137
+ if (Object.keys(settings.tokens).some(token => Object.hasOwn(secrets.callers, token)))
138
+ throw new CommerceError('proxy_token_scope', 'Agent proxy tokens must differ from owner management credentials');
139
+ if (!Number.isInteger(settings.port) || settings.port < 1 || settings.port > 65535)
140
+ throw new CommerceError('proxy_port', 'Invalid proxy listen port');
141
+ const proxy = new A2APeerProxy({ ...settings, buyer });
142
+ closers.push(async () => { await proxy.stop(); proxy.close(); });
143
+ const listener = listenPeerProxy(proxy, settings.host ?? '127.0.0.1', settings.port);
144
+ closers.push(() => closeHttp(listener));
145
+ await once(listener, 'listening');
146
+ }
147
+ }
148
+ const management = new BuyerManagement({ buyer, origin: values.origin, authenticate: bearerAuthenticator(secrets.callers) });
149
+ const http = listenBuyerManagement(management, values.origin, values.host ?? '127.0.0.1', port(4021));
150
+ closers.push(() => closeHttp(http));
151
+ await once(http, 'listening');
152
+ buyer.start();
153
+ signals();
154
+ console.log(JSON.stringify({ listening: port(4021), role: 'buyer', paymentsEnabled: policy.paymentsEnabled }));
155
+ return;
156
+ }
157
+ let config = loadCommerceConfig(raw);
158
+ if (command === 'validate') {
159
+ console.log(JSON.stringify({ valid: true, digest: digest(config), services: config.services.length, paymentPerformed: false }));
160
+ return;
161
+ }
162
+ if (command === 'card' && values.service && values.offer && values.origin) {
163
+ console.log(JSON.stringify(AgentCard.toJSON(createOfferCard(config, values.service, values.offer, values.origin)), null, 2));
164
+ return;
165
+ }
166
+ if (command !== 'serve' || !values.origin || !values.state || !values.credentials)
167
+ throw new CommerceError('usage', usage);
168
+ const secrets = JSON.parse(privateFile(values.credentials).toString('utf8'));
169
+ let integration, syncSettings;
170
+ if (values['envar-config']) {
171
+ syncSettings = JSON.parse(privateFile(values['envar-config']).toString('utf8'));
172
+ if (!syncSettings?.policy)
173
+ throw new CommerceError('envar_configuration', 'Envar settings must include an explicit local policy');
174
+ const tokenFile = resolve(values['envar-credentials']);
175
+ privateFile(tokenFile);
176
+ integration = new EnvarIntegration({ policy: syncSettings.policy, stateDirectory: syncSettings.stateDirectory ?? join(dirname(values.state), 'envar-state'), configDirectory: syncSettings.configDirectory ?? join(dirname(values.state), 'envar-configs'), token: () => privateFile(tokenFile).toString('utf8').trim() });
177
+ closers.push(() => integration.close());
178
+ }
179
+ const controller = new AbortController();
180
+ const store = new CommerceStore(values.state);
181
+ closers.push(() => store.close());
182
+ if (integration)
183
+ config = restoreEnvarConfig(config, integration, store.catalogHistory());
184
+ const protocols = new Set(Object.values(config.paymentProfiles).map(p => p.adapter));
185
+ for (const historical of store.catalogHistory())
186
+ for (const profile of Object.values(historical.profiles))
187
+ protocols.add(profile.adapter);
188
+ if (syncSettings?.policy.allowedX402.length)
189
+ protocols.add('x402');
190
+ if (syncSettings?.policy.allowedMppAccounts.length)
191
+ protocols.add('mpp');
192
+ let vault;
193
+ if (protocols.size) {
194
+ if (!secrets.vaultKeyFile)
195
+ throw new CommerceError('vault_key', 'Paid services require a private vault key file');
196
+ vault = new CredentialVault(join(dirname(values.state), 'authorizations'), privateFile(secrets.vaultKeyFile));
197
+ }
198
+ const gates = {};
199
+ let stripeRecipientFor;
200
+ if (protocols.has('x402')) {
201
+ if (!secrets.payers || !secrets.rpcUrls)
202
+ throw new CommerceError('x402_configuration', 'x402 requires explicit caller-payer bindings and network RPCs');
203
+ const gate = new X402Gate({ store, vault: vault, payerFor: caller => secrets.payers?.[caller], verifyReceipt: evmReceiptVerifier(secrets.rpcUrls), ...evmSettlementRecovery(secrets.rpcUrls) });
204
+ await gate.check(Object.values(config.paymentProfiles).filter((p) => p.adapter === 'x402'));
205
+ gates.x402 = gate;
206
+ }
207
+ if (protocols.has('mpp')) {
208
+ if (!secrets.stripe || !secrets.mppHmacKeyFile)
209
+ throw new CommerceError('mpp_configuration', 'MPP requires private Stripe credentials and a separate challenge key');
210
+ const { MppGate } = await import('./mpp.js'), { stripeProvider } = await import('./stripe-provider.js');
211
+ const providers = Object.fromEntries(Object.entries(secrets.stripe).map(([ref, p]) => [ref, stripeProvider({ ...p, secretKey: privateFile(p.secretKeyFile).toString('utf8').trim() })]));
212
+ const hmacKey = privateFile(secrets.mppHmacKeyFile);
213
+ if (hmacKey.length < 32)
214
+ throw new CommerceError('mpp_hmac_key', 'MPP requires a private challenge key of at least32 random bytes');
215
+ const gate = new MppGate({ store, vault: vault, origin: values.origin, hmacSecret: hmacKey.toString('base64'), providers });
216
+ await gate.check();
217
+ gates.mpp = gate;
218
+ stripeRecipientFor = ref => gate.merchantRecipient(ref);
219
+ }
220
+ const upstreams = new PinnedUpstreams(join(dirname(values.state), 'upstream-bindings.json'), secrets.upstreams, secrets.upstreamInputEncoding, fetch, controller.signal);
221
+ upstreams.prepare(config, store.catalogHistory().map(entry => entry.service));
222
+ const validateAdapters = (candidate) => {
223
+ for (const profile of Object.values(candidate.paymentProfiles)) {
224
+ if (!gates[profile.adapter])
225
+ throw new CommerceError('payment_method_unavailable', 'Configure the payment adapter locally before enabling this service');
226
+ if (profile.adapter === 'x402' && !secrets.rpcUrls?.[profile.network])
227
+ throw new CommerceError('x402_configuration', 'Configure the required network RPC locally');
228
+ if (profile.adapter === 'mpp' && !stripeRecipientFor?.(profile.accountRef))
229
+ throw new CommerceError('mpp_configuration', 'Configure and verify the merchant account locally');
230
+ }
231
+ };
232
+ validateAdapters(config);
233
+ const server = new CommerceServer({ config, origin: values.origin, store, stripeRecipientFor, ownershipChallenges: secrets.ownershipChallenges,
234
+ authenticate: bearerAuthenticator(secrets.callers), execute: upstreams.execute,
235
+ paymentGate: { handle: async (request, order) => {
236
+ const adapter = order.quote.paymentProfile?.adapter, gate = adapter ? gates[adapter] : undefined;
237
+ if (!gate)
238
+ throw new CommerceError('payment_method_unavailable', 'Configured payment adapter is unavailable');
239
+ return gate.handle(request, order);
240
+ } }, onError: (code, orderId) => console.error(JSON.stringify({ code, orderId })) });
241
+ closers.push(async () => { server.stop(); controller.abort(); while (server.isRunning)
242
+ await new Promise(r => setTimeout(r, 50)); });
243
+ let connection;
244
+ if (integration) {
245
+ connection = new EnvarSellerRuntime({ integration, server, upstreams, pollIntervalMs: syncSettings?.pollIntervalMs, validateConfig: validateAdapters, onError: code => console.error(JSON.stringify({ code, integration: 'envar' })) });
246
+ closers.push(() => connection.stop());
247
+ }
248
+ const http = listenCommerce(server, values.origin, values.host ?? '127.0.0.1', port(4020));
249
+ closers.push(async () => { server.stop(); controller.abort(); await closeHttp(http); });
250
+ await once(http, 'listening');
251
+ connection?.start();
252
+ signals();
253
+ console.log(JSON.stringify({ listening: port(4020), role: 'seller', paidTransports: [...protocols] }));
254
+ }
255
+ try {
256
+ await run();
257
+ }
258
+ catch (error) {
259
+ for (const close of closers.reverse()) {
260
+ try {
261
+ await close();
262
+ }
263
+ catch { }
264
+ }
265
+ // Only typed errors are safe to print; arbitrary provider failures may echo credentials.
266
+ console.error(error instanceof CommerceError ? `${error.code}: ${error.message}` : 'Startup failed; inspect configuration and private credential file permissions');
267
+ process.exitCode = 1;
268
+ }
@@ -0,0 +1,8 @@
1
+ /** Node.js buyer runtime. Does not initialize seller storage or any signer. */
2
+ export * from './buyer-store.js';
3
+ export * from './buyer.js';
4
+ export * from './management.js';
5
+ export * from './vault.js';
6
+ export * from './chain.js';
7
+ export type { MppBuyerOptions, MppTokenOperation, MppVerificationContext, MppReceipt } from './mpp-client.js';
8
+ export * from './peer-proxy.js';
@@ -0,0 +1,7 @@
1
+ /** Node.js buyer runtime. Does not initialize seller storage or any signer. */
2
+ export * from './buyer-store.js';
3
+ export * from './buyer.js';
4
+ export * from './management.js';
5
+ export * from './vault.js';
6
+ export * from './chain.js';
7
+ export * from './peer-proxy.js';
@@ -0,0 +1,7 @@
1
+ import { type BuyerPolicy, type CommerceConfig, type PaymentProfile } from './types.js';
2
+ export declare function atomic(value: string): bigint;
3
+ export declare function validateUrl(value: string, allowPrivate?: boolean): URL;
4
+ export declare function currencyOf(profile: PaymentProfile): string;
5
+ export declare function digest(value: unknown): string;
6
+ export declare function loadCommerceConfig(value: unknown): CommerceConfig;
7
+ export declare function loadBuyerPolicy(value: unknown): BuyerPolicy;