@envarai/envarpay 0.1.0-alpha.3 → 0.2.0-alpha.10

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 (72) hide show
  1. package/README.md +57 -145
  2. package/dist/commerce/alerts.d.ts +37 -0
  3. package/dist/commerce/alerts.js +127 -0
  4. package/dist/commerce/buyer-store.d.ts +101 -0
  5. package/dist/commerce/buyer-store.js +168 -0
  6. package/dist/commerce/buyer.d.ts +138 -0
  7. package/dist/commerce/buyer.js +775 -0
  8. package/dist/commerce/card.d.ts +5 -0
  9. package/dist/commerce/card.js +29 -0
  10. package/dist/commerce/chain.d.ts +24 -0
  11. package/dist/commerce/chain.js +146 -0
  12. package/dist/commerce/cli.d.ts +2 -0
  13. package/dist/commerce/cli.js +318 -0
  14. package/dist/commerce/client-runtime.d.ts +8 -0
  15. package/dist/commerce/client-runtime.js +7 -0
  16. package/dist/commerce/config.d.ts +11 -0
  17. package/dist/commerce/config.js +200 -0
  18. package/dist/commerce/envar-runtime.d.ts +47 -0
  19. package/dist/commerce/envar-runtime.js +276 -0
  20. package/dist/commerce/envar.d.ts +126 -0
  21. package/dist/commerce/envar.js +647 -0
  22. package/dist/commerce/http.d.ts +2 -0
  23. package/dist/commerce/http.js +34 -0
  24. package/dist/commerce/index.d.ts +5 -0
  25. package/dist/commerce/index.js +5 -0
  26. package/dist/commerce/management.d.ts +14 -0
  27. package/dist/commerce/management.js +140 -0
  28. package/dist/commerce/mpp-client.d.ts +70 -0
  29. package/dist/commerce/mpp-client.js +29 -0
  30. package/dist/commerce/mpp-runtime.d.ts +4 -0
  31. package/dist/commerce/mpp-runtime.js +4 -0
  32. package/dist/commerce/mpp.d.ts +53 -0
  33. package/dist/commerce/mpp.js +163 -0
  34. package/dist/commerce/native-skills.d.ts +27 -0
  35. package/dist/commerce/native-skills.js +176 -0
  36. package/dist/commerce/operations.d.ts +22 -0
  37. package/dist/commerce/operations.js +39 -0
  38. package/dist/commerce/peer-proxy.d.ts +44 -0
  39. package/dist/commerce/peer-proxy.js +673 -0
  40. package/dist/commerce/pricing.d.ts +12 -0
  41. package/dist/commerce/pricing.js +60 -0
  42. package/dist/commerce/runtime.d.ts +10 -0
  43. package/dist/commerce/runtime.js +10 -0
  44. package/dist/commerce/server.d.ts +61 -0
  45. package/dist/commerce/server.js +516 -0
  46. package/dist/commerce/setup.d.ts +18 -0
  47. package/dist/commerce/setup.js +344 -0
  48. package/dist/commerce/skill-gate.d.ts +54 -0
  49. package/dist/commerce/skill-gate.js +153 -0
  50. package/dist/commerce/store.d.ts +88 -0
  51. package/dist/commerce/store.js +473 -0
  52. package/dist/commerce/stripe-issuer.d.ts +41 -0
  53. package/dist/commerce/stripe-issuer.js +351 -0
  54. package/dist/commerce/stripe-provider.d.ts +11 -0
  55. package/dist/commerce/stripe-provider.js +132 -0
  56. package/dist/commerce/types.d.ts +138 -0
  57. package/dist/commerce/types.js +8 -0
  58. package/dist/commerce/upstream.d.ts +6 -0
  59. package/dist/commerce/upstream.js +103 -0
  60. package/dist/commerce/vault.d.ts +8 -0
  61. package/dist/commerce/vault.js +55 -0
  62. package/dist/commerce/x402.d.ts +38 -0
  63. package/dist/commerce/x402.js +181 -0
  64. package/examples/buyer-policy.json +18 -0
  65. package/examples/envar-integration-policy.json +18 -0
  66. package/examples/hermes-setup-probe.py +105 -0
  67. package/examples/native-skill-runner.py +197 -0
  68. package/examples/seller.json +52 -0
  69. package/package.json +75 -10
  70. package/schemas/buyer-policy.schema.json +253 -0
  71. package/schemas/envarpay.config.schema.json +383 -0
  72. package/schemas/quote.schema.json +91 -0
@@ -0,0 +1,103 @@
1
+ import { AgentCard, GetTaskRequest, SendMessageRequest, TaskState } from '@a2a-js/sdk';
2
+ import { ClientFactory, JsonRpcTransportFactory } from '@a2a-js/sdk/client';
3
+ import { setTimeout as sleep } from 'node:timers/promises';
4
+ import { CommerceError } from './types.js';
5
+ import { validateUrl } from './config.js';
6
+ export function nativeA2AExecutor(tokens, fetchImpl = fetch, signal, options = {}) {
7
+ const inputEncoding = Object.freeze({ ...options.inputEncoding });
8
+ if (Object.values(inputEncoding).some(value => value !== 'data' && value !== 'json-text'))
9
+ throw new CommerceError('invalid_upstream_encoding', 'Choose an explicit data or json-text input encoding');
10
+ const parts = (service, input) => inputEncoding[service.id] === 'json-text' ? [{ text: JSON.stringify(input), mediaType: 'application/json' }] : [{ data: input }];
11
+ const interfaces = new WeakMap();
12
+ async function prepare(service, remote) {
13
+ const cardUrl = validateUrl(service.execution.cardUrl, true), token = tokens[`${service.id}:${service.revision}`] ?? tokens[service.id];
14
+ if (!token || token.length < 16)
15
+ throw new CommerceError('upstream_credentials', 'Configured runtime authentication is missing');
16
+ if (remote && remote.origin !== service.execution.cardUrl)
17
+ throw new CommerceError('upstream_origin', 'Original runtime origin differs from the frozen service');
18
+ const scopedFetch = async (input, init) => {
19
+ const request = new Request(input, init);
20
+ if (new URL(request.url).origin !== cardUrl.origin)
21
+ throw new CommerceError('upstream_origin', 'Runtime Card cannot redirect credentials to another origin');
22
+ const headers = new Headers(request.headers);
23
+ headers.set('Authorization', 'Bearer ' + token);
24
+ headers.set('A2A-Version', '1.0');
25
+ const signals = [request.signal, AbortSignal.timeout(120000), ...(signal ? [signal] : [])];
26
+ const response = await fetchImpl(new Request(request, { headers, redirect: 'error', signal: AbortSignal.any(signals) }));
27
+ if (Number(response.headers.get('Content-Length') ?? 0) > 4 * 1024 * 1024)
28
+ throw new CommerceError('upstream_size', 'Runtime response exceeds limit');
29
+ if (response.body) {
30
+ const reader = response.body.getReader(), chunks = [];
31
+ let length = 0;
32
+ for (;;) {
33
+ const next = await reader.read();
34
+ if (next.done)
35
+ break;
36
+ length += next.value.byteLength;
37
+ if (length > 4 * 1024 * 1024) {
38
+ await reader.cancel();
39
+ throw new CommerceError('upstream_size', 'Runtime response exceeds limit');
40
+ }
41
+ chunks.push(next.value);
42
+ }
43
+ return new Response(Buffer.concat(chunks), { status: response.status, statusText: response.statusText, headers: response.headers });
44
+ }
45
+ return response;
46
+ };
47
+ const response = await scopedFetch(cardUrl);
48
+ if (!response.ok)
49
+ throw new CommerceError('upstream_card', 'Unable to read runtime Agent Card');
50
+ const card = AgentCard.fromJSON(await response.json());
51
+ const selected = card.supportedInterfaces.find(x => x.protocolVersion === '1.0' && x.protocolBinding === 'JSONRPC');
52
+ if (!selected)
53
+ throw new CommerceError('upstream_version', 'Runtime must expose A2A 1.0 JSONRPC');
54
+ for (const endpoint of card.supportedInterfaces)
55
+ if (validateUrl(endpoint.url, true).origin !== cardUrl.origin)
56
+ throw new CommerceError('upstream_origin', 'Runtime interface must share configured Card origin');
57
+ if (remote?.interfaceUrl && selected.url !== remote.interfaceUrl)
58
+ throw new CommerceError('upstream_endpoint_changed', 'Original Task interface changed; restore it before recovery');
59
+ const factory = new ClientFactory({ transports: [new JsonRpcTransportFactory({ fetchImpl: scopedFetch })] });
60
+ const client = await factory.createFromAgentCard({ ...card, supportedInterfaces: [selected] });
61
+ return { client, interfaceUrl: selected.url };
62
+ }
63
+ function observed(task, interfaceUrl, expectedId) {
64
+ if (!task || typeof task !== 'object' || !('status' in task) || !('id' in task) || !task.id)
65
+ throw new CommerceError('upstream_task_required', 'Runtime returned no durable Task handle');
66
+ const result = task;
67
+ if (expectedId && result.id !== expectedId)
68
+ throw new CommerceError('remote_task_conflict', 'Runtime returned a different Task');
69
+ interfaces.set(result, interfaceUrl);
70
+ return result;
71
+ }
72
+ async function* observe(task, prepared, service) {
73
+ const deadline = Date.now() + service.contract.targetDurationSeconds * 1000, id = task.id;
74
+ for (;;) {
75
+ yield task;
76
+ if ([TaskState.TASK_STATE_COMPLETED, TaskState.TASK_STATE_FAILED, TaskState.TASK_STATE_CANCELED, TaskState.TASK_STATE_REJECTED, TaskState.TASK_STATE_INPUT_REQUIRED, TaskState.TASK_STATE_AUTH_REQUIRED].includes(task.status?.state ?? 0))
77
+ return;
78
+ if (Date.now() >= deadline)
79
+ throw new CommerceError('execution_pending', 'Read the original Task to continue observing execution');
80
+ await sleep(1000, undefined, { signal });
81
+ task = observed(await prepared.client.getTask(GetTaskRequest.fromJSON({ id })), prepared.interfaceUrl, id);
82
+ }
83
+ }
84
+ const execute = async function* (order, service) {
85
+ const prepared = await prepare(service);
86
+ const request = SendMessageRequest.fromJSON({ message: { messageId: order.messageId, role: 'ROLE_USER', parts: parts(service, order.input) }, configuration: { returnImmediately: true } });
87
+ const task = observed(await prepared.client.sendMessage(request), prepared.interfaceUrl);
88
+ yield* observe(task, prepared, service);
89
+ };
90
+ execute.continue = async function* (_order, service, continuation, remote) {
91
+ const prepared = await prepare(service, remote);
92
+ const request = SendMessageRequest.fromJSON({ message: { messageId: continuation.messageId, taskId: remote.taskId, ...(remote.contextId ? { contextId: remote.contextId } : {}), role: 'ROLE_USER', parts: parts(service, continuation.input) }, configuration: { returnImmediately: true } });
93
+ const task = observed(await prepared.client.sendMessage(request), prepared.interfaceUrl, remote.taskId);
94
+ yield* observe(task, prepared, service);
95
+ };
96
+ execute.recover = async (_order, service, remote) => {
97
+ const prepared = await prepare(service, remote);
98
+ // Recovery never calls SendMessage, creates a new Task, or retries a dispatch.
99
+ return observed(await prepared.client.getTask(GetTaskRequest.fromJSON({ id: remote.taskId })), prepared.interfaceUrl, remote.taskId);
100
+ };
101
+ execute.remoteInterface = task => interfaces.get(task);
102
+ return execute;
103
+ }
@@ -0,0 +1,8 @@
1
+ /** Original authorizations are encrypted before any external settlement attempt. */
2
+ export declare class CredentialVault {
3
+ private directory;
4
+ private readonly key;
5
+ constructor(directory: string, key: Buffer);
6
+ put(value: unknown): string;
7
+ get(id: string): unknown;
8
+ }
@@ -0,0 +1,55 @@
1
+ import { createCipheriv, createDecipheriv, randomBytes, randomUUID } from 'node:crypto';
2
+ import { mkdirSync, readFileSync, writeFileSync, chmodSync, openSync, closeSync, fsyncSync, lstatSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import { CommerceError } from './types.js';
5
+ /** Original authorizations are encrypted before any external settlement attempt. */
6
+ export class CredentialVault {
7
+ directory;
8
+ key;
9
+ constructor(directory, key) {
10
+ this.directory = directory;
11
+ if (key.length !== 32)
12
+ throw new CommerceError('vault_key', 'A 32-byte vault encryption key is required');
13
+ this.key = Buffer.from(key);
14
+ mkdirSync(directory, { recursive: true, mode: 0o700 });
15
+ if (!lstatSync(directory).isDirectory())
16
+ throw new CommerceError('vault_directory', 'Vault must be a real directory');
17
+ chmodSync(directory, 0o700);
18
+ }
19
+ put(value) {
20
+ const serialized = JSON.stringify(value);
21
+ if (!serialized || Buffer.byteLength(serialized) > 65536)
22
+ throw new CommerceError('vault_size', 'Authorization exceeds vault limit');
23
+ const id = randomUUID(), nonce = randomBytes(12);
24
+ const cipher = createCipheriv('aes-256-gcm', this.key, nonce);
25
+ cipher.setAAD(Buffer.from(id));
26
+ const encrypted = Buffer.concat([cipher.update(serialized, 'utf8'), cipher.final()]);
27
+ writeFileSync(join(this.directory, id), Buffer.concat([nonce, cipher.getAuthTag(), encrypted]), { mode: 0o600, flag: 'wx', flush: true });
28
+ // Persist the directory entry before returning a reference to the ledger.
29
+ const directoryFd = openSync(this.directory, 'r');
30
+ try {
31
+ fsyncSync(directoryFd);
32
+ }
33
+ finally {
34
+ closeSync(directoryFd);
35
+ }
36
+ return id;
37
+ }
38
+ get(id) {
39
+ if (!/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/.test(id))
40
+ throw new CommerceError('vault_reference', 'Invalid credential reference');
41
+ const path = join(this.directory, id), stat = lstatSync(path);
42
+ if (!stat.isFile() || stat.size < 29 || stat.size > 65564 || (stat.mode & 0o077) !== 0)
43
+ throw new CommerceError('vault_file', 'Invalid private authorization file');
44
+ const bytes = readFileSync(path);
45
+ const cipher = createDecipheriv('aes-256-gcm', this.key, bytes.subarray(0, 12));
46
+ cipher.setAAD(Buffer.from(id));
47
+ cipher.setAuthTag(bytes.subarray(12, 28));
48
+ try {
49
+ return JSON.parse(Buffer.concat([cipher.update(bytes.subarray(28)), cipher.final()]).toString('utf8'));
50
+ }
51
+ catch {
52
+ throw new CommerceError('vault_integrity', 'Original authorization failed integrity verification');
53
+ }
54
+ }
55
+ }
@@ -0,0 +1,38 @@
1
+ import { type FacilitatorClient } from '@x402/core/server';
2
+ import type { PaymentPayload, PaymentRequirements, SettleResponse } from '@x402/core/types';
3
+ import { type PaymentProfile } from './types.js';
4
+ import { CommerceStore, type OrderRecord } from './store.js';
5
+ import { CredentialVault } from './vault.js';
6
+ type X402Profile = Extract<PaymentProfile, {
7
+ adapter: 'x402';
8
+ }>;
9
+ export interface X402GateOptions {
10
+ store: CommerceStore;
11
+ vault: CredentialVault;
12
+ payerFor: (caller: string) => string | undefined;
13
+ facilitator?: (profile: X402Profile) => FacilitatorClient;
14
+ verifyReceipt: (receipt: SettleResponse, payload: PaymentPayload, requirements: PaymentRequirements) => Promise<boolean>;
15
+ /** Read chain before broadcast, so a lost facilitator response remains recoverable. */
16
+ checkpoint?: (requirements: PaymentRequirements) => Promise<string>;
17
+ findOriginalReceipt?: (payload: PaymentPayload, requirements: PaymentRequirements, fromBlock: string) => Promise<SettleResponse | undefined>;
18
+ proveExpiredUnused?: (payload: PaymentPayload, requirements: PaymentRequirements) => Promise<boolean>;
19
+ }
20
+ export type GateResult = {
21
+ response: Response;
22
+ } | {
23
+ headers: Record<string, string>;
24
+ };
25
+ /** Official x402 v2 exact settlement, with independent chain evidence before work. */
26
+ export declare class X402Gate {
27
+ private options;
28
+ private servers;
29
+ constructor(options: X402GateOptions);
30
+ check(profiles: readonly X402Profile[]): Promise<void>;
31
+ recover(orderId: string, caller: string): Promise<{
32
+ state: string;
33
+ taskId: string;
34
+ }>;
35
+ private resource;
36
+ handle(request: Request, order: OrderRecord): Promise<GateResult>;
37
+ }
38
+ export {};
@@ -0,0 +1,181 @@
1
+ import { findDefaultAsset } from '@x402/evm';
2
+ import { x402ResourceServer, HTTPFacilitatorClient } from '@x402/core/server';
3
+ import { decodePaymentSignatureHeader, encodePaymentRequiredHeader, encodePaymentResponseHeader } from '@x402/core/http';
4
+ import { ExactEvmScheme } from '@x402/evm/exact/server';
5
+ import { PAYMENT_IDENTIFIER, declarePaymentIdentifierExtension, extractPaymentIdentifier, validatePaymentIdentifier, paymentIdentifierResourceServerExtension, } from '@x402/extensions/payment-identifier';
6
+ import { CommerceError } from './types.js';
7
+ import { digest } from './config.js';
8
+ /** Official x402 v2 exact settlement, with independent chain evidence before work. */
9
+ export class X402Gate {
10
+ options;
11
+ servers = new Map();
12
+ constructor(options) {
13
+ this.options = options;
14
+ }
15
+ async check(profiles) {
16
+ for (const profile of profiles) {
17
+ const { facilitator } = await this.resource(profile);
18
+ const supported = await facilitator.getSupported();
19
+ if (!supported.kinds.some(k => k.x402Version === 2 && k.scheme === 'exact' && k.network === profile.network))
20
+ throw new CommerceError('facilitator_network', 'Configured facilitator does not support exact payments on this network');
21
+ if (this.options.checkpoint)
22
+ await this.options.checkpoint({ scheme: 'exact', network: profile.network, asset: profile.asset, payTo: profile.payTo, amount: '1', maxTimeoutSeconds: 300, extra: {} });
23
+ }
24
+ }
25
+ async recover(orderId, caller) {
26
+ const order = this.options.store.getOrder(orderId, caller);
27
+ if (!order)
28
+ throw new CommerceError('order_not_found', 'Order not found for this buyer');
29
+ const attempt = this.options.store.paymentForOrder(order.id);
30
+ if (!attempt)
31
+ throw new CommerceError('attempt_not_found', 'No original payment to recover');
32
+ if (attempt.state === 'confirmed')
33
+ return { state: 'confirmed', taskId: order.taskId };
34
+ if (attempt.state === 'rejected')
35
+ return { state: 'rejected', taskId: order.taskId };
36
+ const saved = this.options.vault.get(attempt.credentialRef);
37
+ const required = this.options.store.paymentChallenge(order.id);
38
+ if (saved.quoteId !== order.quote.quoteId || !required?.accepts.some(r => digest(r) === digest(saved.requirements))) {
39
+ throw new CommerceError('recovery_conflict', 'Stored authorization differs from the frozen challenge');
40
+ }
41
+ let receipt = attempt.receipt;
42
+ if (!receipt?.transaction && saved.fromBlock && this.options.findOriginalReceipt) {
43
+ receipt = await this.options.findOriginalReceipt(saved.payload, saved.requirements, saved.fromBlock) ?? null;
44
+ }
45
+ if (!receipt?.transaction) {
46
+ if (await this.options.proveExpiredUnused?.(saved.payload, saved.requirements)) {
47
+ this.options.store.recordSettlement(attempt.id, 'rejected', { errorReason: 'authorization_expired_unused' });
48
+ return { state: 'rejected', taskId: order.taskId };
49
+ }
50
+ return { state: 'unknown', taskId: order.taskId };
51
+ }
52
+ let proven = false;
53
+ try {
54
+ proven = await this.options.verifyReceipt(receipt, saved.payload, saved.requirements);
55
+ }
56
+ catch { }
57
+ // Persist the discovered transaction even if confirmations are still pending.
58
+ this.options.store.recordSettlement(attempt.id, proven ? 'confirmed' : 'unknown', receipt);
59
+ return { state: proven ? 'confirmed' : 'unknown', taskId: order.taskId };
60
+ }
61
+ async resource(profile) {
62
+ const key = digest(profile);
63
+ let promise = this.servers.get(key);
64
+ if (!promise) {
65
+ promise = (async () => {
66
+ const facilitator = this.options.facilitator?.(profile) ?? new HTTPFacilitatorClient({ url: profile.facilitatorUrl });
67
+ const server = new x402ResourceServer(facilitator)
68
+ .register(profile.network, new ExactEvmScheme())
69
+ .registerExtension(paymentIdentifierResourceServerExtension);
70
+ await server.initialize();
71
+ return { server, facilitator };
72
+ })();
73
+ this.servers.set(key, promise);
74
+ void promise.catch(() => this.servers.delete(key));
75
+ }
76
+ return promise;
77
+ }
78
+ async handle(request, order) {
79
+ const profile = order.quote.paymentProfile;
80
+ if (profile?.adapter !== 'x402')
81
+ throw new CommerceError('payment_method_unavailable', 'This offer requires another installed payment adapter');
82
+ const previous = this.options.store.paymentForOrder(order.id);
83
+ if (order.paymentState === 'confirmed' && previous?.receipt) {
84
+ return { headers: { 'PAYMENT-RESPONSE': encodePaymentResponseHeader(previous.receipt) } };
85
+ }
86
+ if (['settling', 'unknown'].includes(order.paymentState)) {
87
+ const recovered = await this.recover(order.id, order.caller);
88
+ if (recovered.state === 'confirmed') {
89
+ const receipt = this.options.store.paymentForOrder(order.id).receipt;
90
+ return { headers: { 'PAYMENT-RESPONSE': encodePaymentResponseHeader(receipt) } };
91
+ }
92
+ if (recovered.state === 'rejected')
93
+ return { response: Response.json({ error: 'authorization_expired_unused', taskId: order.taskId }, { status: 409, headers: { 'Cache-Control': 'no-store' } }) };
94
+ return { response: Response.json({ error: 'payment_reconciliation_required', taskId: order.taskId }, { status: 503, headers: { 'Cache-Control': 'no-store' } }) };
95
+ }
96
+ if (Date.now() >= Date.parse(order.quote.expiresAt))
97
+ return { response: Response.json({ error: 'quote_expired' }, { status: 409 }) };
98
+ const { server, facilitator } = await this.resource(profile);
99
+ let required = this.options.store.paymentChallenge(order.id);
100
+ if (!required) {
101
+ const token = findDefaultAsset(profile.asset, profile.network);
102
+ if (!token)
103
+ throw new CommerceError('unsupported_asset', 'Official token signing domain is unavailable');
104
+ const requirements = await server.buildPaymentRequirements({
105
+ scheme: 'exact', network: profile.network, payTo: profile.payTo,
106
+ price: { asset: profile.asset, amount: order.quote.amount, extra: { name: token.name, version: token.version } },
107
+ maxTimeoutSeconds: 300, extra: { paymentFlow: 'upfront', assetTransferMethod: 'eip3009' },
108
+ });
109
+ const resource = new URL(request.url);
110
+ resource.search = '';
111
+ resource.searchParams.set('quote', order.quote.quoteId);
112
+ required = this.options.store.freezePaymentChallenge(order.id, await server.createPaymentRequiredResponse(requirements, { url: resource.href, description: order.quote.serviceId, mimeType: 'application/json' }, undefined, { [PAYMENT_IDENTIFIER]: declarePaymentIdentifierExtension(false), 'urn:envarpay:quote:1': { info: { quoteId: order.quote.quoteId, messageId: order.messageId, serviceId: order.quote.serviceId, serviceRevision: order.quote.serviceRevision, offerId: order.offerId, inputDigest: order.inputDigest, termsDigest: order.quote.termsDigest, expiresAt: order.quote.expiresAt, amount: order.quote.amount, currency: order.quote.currency, recipient: order.quote.recipient } } }));
113
+ }
114
+ // Identifier is optional for unextended standard clients; nonce dedup is mandatory.
115
+ const challenge = () => ({ response: new Response('{}', {
116
+ status: 402, headers: { 'Content-Type': 'application/json', 'Cache-Control': 'no-store', 'PAYMENT-REQUIRED': encodePaymentRequiredHeader(required) },
117
+ }) });
118
+ const header = request.headers.get('PAYMENT-SIGNATURE');
119
+ if (!header)
120
+ return challenge();
121
+ if (header.length > 32768)
122
+ throw new CommerceError('payment_payload_size', 'Payment credential exceeds limit');
123
+ let payload;
124
+ try {
125
+ payload = decodePaymentSignatureHeader(header);
126
+ }
127
+ catch {
128
+ throw new CommerceError('invalid_payment', 'Malformed payment credential');
129
+ }
130
+ const matched = server.findMatchingRequirements(required.accepts, payload);
131
+ if (!matched || payload.x402Version !== 2 || payload.resource?.url !== required.resource.url || digest(payload.accepted) !== digest(matched)) {
132
+ throw new CommerceError('quote_mismatch', 'Payment does not match the frozen quote');
133
+ }
134
+ const extension = payload.extensions?.[PAYMENT_IDENTIFIER];
135
+ // Validate against our schema; never compile a schema supplied by the buyer.
136
+ if (extension !== undefined && !validatePaymentIdentifier({ info: extension?.info, schema: declarePaymentIdentifierExtension(false).schema }).valid) {
137
+ throw new CommerceError('invalid_payment_identifier', 'Invalid standard payment identifier');
138
+ }
139
+ const identifier = extractPaymentIdentifier(payload, false);
140
+ const native = payload.payload;
141
+ const auth = native.authorization;
142
+ const payer = this.options.payerFor(order.caller);
143
+ if (!payer || !auth?.from || auth.from.toLowerCase() !== payer.toLowerCase() || auth.to?.toLowerCase() !== profile.payTo.toLowerCase() || auth.value !== order.quote.amount || !/^0x[0-9a-fA-F]{64}$/.test(auth.nonce ?? '') || !native.signature) {
144
+ throw new CommerceError('payment_identity', 'Signed payer, recipient, amount and nonce must match authorized buyer and quote');
145
+ }
146
+ // Quote UI metadata is locally checked above. It is not a facilitator
147
+ // settlement extension; do not require third-party facilitators to implement it.
148
+ const settlementPayload = { ...payload, extensions: Object.fromEntries(Object.entries(payload.extensions ?? {}).filter(([key]) => key !== 'urn:envarpay:quote:1')) };
149
+ // Upfront SDK verification may skip /verify; explicitly perform it before reserve.
150
+ const verified = await facilitator.verify(settlementPayload, matched);
151
+ if (!verified.isValid)
152
+ return challenge();
153
+ if (verified.payer && verified.payer.toLowerCase() !== payer.toLowerCase())
154
+ throw new CommerceError('payment_identity', 'Verified payer differs from authorized buyer');
155
+ const economicKey = digest({ network: profile.network, asset: profile.asset.toLowerCase(), payer: payer.toLowerCase(), nonce: auth.nonce.toLowerCase() });
156
+ const fromBlock = await this.options.checkpoint?.(matched);
157
+ const saved = { payload, requirements: matched, quoteId: order.quote.quoteId, ...(fromBlock !== undefined ? { fromBlock } : {}) };
158
+ const credentialRef = this.options.vault.put(saved);
159
+ const attempt = this.options.store.reservePayment(order.id, order.caller, economicKey, credentialRef, identifier ?? undefined);
160
+ let result;
161
+ try {
162
+ result = await server.settlePayment(settlementPayload, matched, undefined, undefined, undefined, 'before-handler');
163
+ }
164
+ catch {
165
+ this.options.store.recordSettlement(attempt, 'unknown', {});
166
+ return { response: Response.json({ error: 'payment_outcome_unknown' }, { status: 503 }) };
167
+ }
168
+ // A failure can follow broadcast. Never create a second economic authorization.
169
+ let proven = false;
170
+ if (result.success) {
171
+ try {
172
+ proven = await this.options.verifyReceipt(result, payload, matched);
173
+ }
174
+ catch { }
175
+ }
176
+ this.options.store.recordSettlement(attempt, proven ? 'confirmed' : 'unknown', result);
177
+ if (!proven)
178
+ return { response: Response.json({ error: result.success ? 'receipt_verification_pending' : 'payment_outcome_unknown' }, { status: 503 }) };
179
+ return { headers: { 'PAYMENT-RESPONSE': encodePaymentResponseHeader(result) } };
180
+ }
181
+ }
@@ -0,0 +1,18 @@
1
+ {
2
+ "policyVersion":1,
3
+ "paymentsEnabled":false,
4
+ "approval":"per_purchase",
5
+ "peers":[{
6
+ "id":"research-seller",
7
+ "cardUrl":"https://seller.example/services/research/v1/offers/usdc-once/agent-card.json",
8
+ "protocol":"x402",
9
+ "currency":"eip155:84532/erc20:0x036CbD53842c5426634e7929541eC2318f3dCF7e",
10
+ "recipient":"0x2222222222222222222222222222222222222222",
11
+ "maxPerPurchase":"3000000"
12
+ }],
13
+ "budgets":[{
14
+ "currency":"eip155:84532/erc20:0x036CbD53842c5426634e7929541eC2318f3dCF7e",
15
+ "maxTotal":"10000000",
16
+ "period":"cumulative"
17
+ }]
18
+ }
@@ -0,0 +1,18 @@
1
+ {
2
+ "enabled": true,
3
+ "platformOrigin": "https://envar.ai",
4
+ "agentId": "54fe760a-4580-4258-9002-80f34c0d757a",
5
+ "runtimeAgentId": "hermes-researcher",
6
+ "acceptUpdates": true,
7
+ "allowedServices": ["research"],
8
+ "allowedUpstreamOrigins": ["http://hermes:9000"],
9
+ "allowedX402": [
10
+ {
11
+ "network": "eip155:84532",
12
+ "asset": "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
13
+ "payTo": "0x2222222222222222222222222222222222222222",
14
+ "facilitatorOrigin": "https://facilitator.example"
15
+ }
16
+ ],
17
+ "allowedMppAccounts": ["seller-stripe"]
18
+ }
@@ -0,0 +1,105 @@
1
+ """Read owner-local Hermes settings through a private pipe; never calls a model."""
2
+
3
+ import json
4
+ import os
5
+ import shlex
6
+ import sys
7
+ from pathlib import Path
8
+
9
+ try:
10
+ import hermes_yaml as yaml
11
+ except ImportError:
12
+ import yaml
13
+
14
+
15
+ def inspect(home):
16
+ home = Path(home or os.environ.get("HERMES_HOME", "~/.hermes")).expanduser().resolve()
17
+ config = yaml.safe_load((home / "config.yaml").read_text()) or {}
18
+ model = config.get("model", {})
19
+ if isinstance(model, str):
20
+ model = {"default": model}
21
+ provider = config.get("providers", {}).get(model.get("provider"), {})
22
+ dotenv = {}
23
+ if (home / ".env").exists():
24
+ for line in (home / ".env").read_text().splitlines():
25
+ key, sep, value = line.partition("=")
26
+ if sep and not key.strip().startswith("#"):
27
+ dotenv[key.strip()] = value.strip().strip("\"'")
28
+ key_file = None
29
+ command = shlex.split(provider.get("key_cmd", ""))
30
+ if len(command) == 2 and command[0] == "cat":
31
+ key_file = str(Path(command[1]).expanduser().resolve())
32
+ key = (
33
+ provider.get("api_key")
34
+ or provider.get("key")
35
+ or dotenv.get("OPENAI_API_KEY")
36
+ or os.environ.get("OPENAI_API_KEY")
37
+ )
38
+ base = (
39
+ provider.get("api")
40
+ or provider.get("base_url")
41
+ or model.get("base_url")
42
+ or dotenv.get("OPENAI_BASE_URL")
43
+ or os.environ.get("OPENAI_BASE_URL")
44
+ )
45
+ if not model.get("default") or not base or not (key_file or key):
46
+ raise ValueError("Model configuration unavailable")
47
+ if key_file and (not Path(key_file).is_file() or not Path(key_file).read_text().strip()):
48
+ raise ValueError("Model key unavailable")
49
+ roots = [home / "skills"]
50
+ roots.extend(
51
+ Path(os.path.expandvars(str(p))).expanduser().resolve()
52
+ for p in config.get("skills", {}).get("external_dirs", [])
53
+ )
54
+ skills = []
55
+ seen = set()
56
+ for root in roots:
57
+ for entry in sorted(root.rglob("SKILL.md")) if root.is_dir() else []:
58
+ if entry.is_symlink():
59
+ continue
60
+ raw = entry.read_text()
61
+ front = raw.split("---", 2)
62
+ meta = yaml.safe_load(front[1]) if raw.startswith("---\n") and len(front) == 3 else {}
63
+ name = (meta or {}).get("name")
64
+ if not isinstance(name, str) or name in seen:
65
+ continue
66
+ seen.add(name)
67
+ files = []
68
+ unsupported = False
69
+ for file in entry.parent.rglob("*"):
70
+ if file.is_symlink():
71
+ unsupported = True
72
+ elif file.is_file():
73
+ if file.suffix.lower() not in {".md", ".txt"} and file.name != "LICENSE":
74
+ unsupported = True
75
+ files.append(str(file.relative_to(entry.parent)))
76
+ skills.append(
77
+ {
78
+ "name": name,
79
+ "path": str(entry.parent),
80
+ "description": str((meta or {}).get("description", ""))[:500],
81
+ "textPackage": not unsupported,
82
+ "files": len(files),
83
+ }
84
+ )
85
+ return {
86
+ "home": str(home),
87
+ "python": sys.executable,
88
+ "model": model["default"],
89
+ "baseUrl": base,
90
+ "apiKeyFile": key_file,
91
+ "apiKey": None if key_file else key,
92
+ "skills": skills,
93
+ }
94
+
95
+
96
+ if __name__ == "__main__":
97
+ try:
98
+ print(json.dumps(inspect(sys.argv[1] if len(sys.argv) > 1 else None)))
99
+ except Exception:
100
+ print(
101
+ "Cannot read this Hermes model configuration. "
102
+ "Configure an OpenAI-compatible model with a base URL and a local API key first.",
103
+ file=sys.stderr,
104
+ )
105
+ sys.exit(1)