@envarai/envarpay 0.2.0-alpha.19 → 0.2.0-alpha.20

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 (54) hide show
  1. package/README.md +18 -63
  2. package/dist/commerce/alerts.d.ts +5 -15
  3. package/dist/commerce/alerts.js +21 -77
  4. package/dist/commerce/card.js +1 -1
  5. package/dist/commerce/cli.js +17 -145
  6. package/dist/commerce/config.d.ts +1 -4
  7. package/dist/commerce/config.js +4 -82
  8. package/dist/commerce/configure.d.ts +0 -1
  9. package/dist/commerce/configure.js +0 -80
  10. package/dist/commerce/discovery.d.ts +1 -5
  11. package/dist/commerce/discovery.js +1 -4
  12. package/dist/commerce/envar-balance.js +1 -1
  13. package/dist/commerce/envar.d.ts +1 -32
  14. package/dist/commerce/envar.js +3 -82
  15. package/dist/commerce/operations.d.ts +1 -2
  16. package/dist/commerce/operations.js +17 -20
  17. package/dist/commerce/pricing.d.ts +0 -1
  18. package/dist/commerce/pricing.js +1 -3
  19. package/dist/commerce/runtime.d.ts +0 -2
  20. package/dist/commerce/runtime.js +0 -2
  21. package/dist/commerce/server.d.ts +0 -1
  22. package/dist/commerce/server.js +2 -3
  23. package/dist/commerce/setup.js +4 -3
  24. package/dist/commerce/types.d.ts +0 -59
  25. package/examples/envar-integration-policy.json +1 -9
  26. package/examples/seller.json +143 -26
  27. package/package.json +3 -16
  28. package/schemas/envarpay.config.schema.json +1 -80
  29. package/dist/commerce/buyer-store.d.ts +0 -101
  30. package/dist/commerce/buyer-store.js +0 -168
  31. package/dist/commerce/buyer.d.ts +0 -138
  32. package/dist/commerce/buyer.js +0 -775
  33. package/dist/commerce/chain.d.ts +0 -24
  34. package/dist/commerce/chain.js +0 -196
  35. package/dist/commerce/client-runtime.d.ts +0 -8
  36. package/dist/commerce/client-runtime.js +0 -7
  37. package/dist/commerce/management.d.ts +0 -14
  38. package/dist/commerce/management.js +0 -140
  39. package/dist/commerce/mpp-client.d.ts +0 -70
  40. package/dist/commerce/mpp-client.js +0 -29
  41. package/dist/commerce/mpp-runtime.d.ts +0 -4
  42. package/dist/commerce/mpp-runtime.js +0 -4
  43. package/dist/commerce/mpp.d.ts +0 -53
  44. package/dist/commerce/mpp.js +0 -163
  45. package/dist/commerce/peer-proxy.d.ts +0 -44
  46. package/dist/commerce/peer-proxy.js +0 -673
  47. package/dist/commerce/stripe-issuer.d.ts +0 -41
  48. package/dist/commerce/stripe-issuer.js +0 -351
  49. package/dist/commerce/stripe-provider.d.ts +0 -11
  50. package/dist/commerce/stripe-provider.js +0 -132
  51. package/dist/commerce/x402.d.ts +0 -38
  52. package/dist/commerce/x402.js +0 -181
  53. package/examples/buyer-policy.json +0 -18
  54. package/schemas/buyer-policy.schema.json +0 -253
package/README.md CHANGED
@@ -1,76 +1,31 @@
1
- # EnvarPay TypeScript
1
+ # `@envarai/envarpay`
2
2
 
3
- Independent service commerce for user-operated Agents. Public communication uses
4
- A2A 1.0; payments use native x402 v2 exact USDC or MPP Stripe charge. Prices and
5
- scope are versioned configuration, enforced before dispatch, not instructions in
6
- a prompt. Envar is an optional directory and management UI.
3
+ This package runs an A2A seller gate for free services and Envar account-balance purchases. Buyers use their Envar Wallet; this package does not sign buyer payments.
7
4
 
8
- The 0.2 source line replaces the previous npm wallet-only MCP client. Build from
9
- this checkout while the release is undergoing acceptance; a source version is
10
- not evidence that its npm/OCI artifact is published.
5
+ ## Recommended setup
11
6
 
12
- ## Runtime and entry points
13
-
14
- For Hermes onboarding, see [guided Skill setup](../../docs/hermes-skill-setup.md). The `setup --file ...` command reads the owner's local model configuration and creates a private seller with an empty catalog, ready for the first service to be configured in Envar.
15
-
16
- The experimental `external-setup` and `external-serve` commands connect an existing
17
- external Agent through scoped MCP tasks/events and a free A2A entry. See
18
- [external Agent acceptance](../../docs/external-agents.md) for OAuth pairing,
19
- Skill discovery, restart and the required real execution evidence.
20
-
21
- Node.js 22.14+ (Node 24 recommended). SQLite is single-instance. Contracts can be
22
- read without loading the seller or buyer runtime; Bun validation covers the
23
- contracts entry, not the SQLite server.
24
-
25
- | Entry | Purpose |
26
- |---|---|
27
- | `@envarai/envarpay` | Config types/validation, integer pricing, quotes and A2A Cards |
28
- | `/server` | Durable seller A2A handler, exact x402 gate and native upstream executor |
29
- | `/client` | Buyer policy, atomic budgets, original-operation recovery and private management |
30
- | `/mpp` | Opt-in native MPP seller and read-only Stripe reconciliation |
31
- | `/envar` | Optional bounded config pull/ack and durable observation reports |
7
+ Use the Agent-specific setup file downloaded from Envar:
32
8
 
33
9
  ```sh
34
- npm ci --ignore-scripts
35
- npm run build
36
- node dist/commerce/cli.js init --directory ./private
37
- node dist/commerce/cli.js validate --config ./private/seller.json
10
+ envarpay setup --file /private/path/envar-setup.json
38
11
  ```
39
12
 
40
- Initialization writes examples and a vault key; it never creates a purchase or
41
- funds a wallet. Replace example addresses/hosts and review service limits before
42
- starting. Keep Agent model credentials in the Agent, and signer keys in a separate
43
- private wallet boundary.
13
+ Follow the terminal prompts to select a Hermes or OpenClaw runtime and its text Skills. Setup creates an owner-only directory containing `seller.json`, `credentials.json`, `envar.json`, `envar.token`, `vault.key`, `connection.json`, and `start.mjs`. Run the printed `node .../start.mjs` command, import `connection.json` in Envar, and select **Check connection**. The public [Agent guide](https://envar.ai/guide/agents) gives the platform-specific steps and the test message to send.
44
14
 
45
- ```sh
46
- node dist/commerce/cli.js serve --config seller.json \
47
- --credentials private/seller-auth.json --state private/seller.sqlite3 \
48
- --origin https://seller.example --port 4020
49
- node dist/commerce/cli.js buyer-serve --config buyer-policy.json \
50
- --credentials private/buyer-auth.json --state private/buyer.sqlite3 \
51
- --origin https://buyer-management.example --port 4021
52
- ```
15
+ The generated seller starts with free services and a local balance receiving reference of `envar`. A paid service becomes available only after Envar approves this seller account and the service editor verifies the actual unpaid USD quote. In Envar, choose a Skill, enter its USD price, select **Test service**, then **Publish**. There is no separate payment setup command.
53
16
 
54
- The management service is private and owner-authenticated. A website may request a
55
- quote and confirm an already-reviewed purchase; it cannot change peers, recipient
56
- allowlists, budgets or signing keys. Unknown outcomes retain the original request,
57
- signature/SPT and reserved budget. Never create a replacement purchase after a
58
- timeout.
17
+ ## Manual seller commands
59
18
 
60
- [Seller and wire behavior](../../docs/a2a-commerce.md) ·
61
- [Buyer management](../../docs/a2a-buyer.md) ·
62
- [Continuation and recovery](../../docs/a2a-runtime-recovery.md) ·
63
- [Envar configuration integration](../../docs/a2a-envar.md)
19
+ ```sh
20
+ envarpay init --directory /private/envarpay
21
+ envarpay validate --config /private/envarpay/seller.json
22
+ envarpay card --config /private/envarpay/seller.json --service SERVICE --offer OFFER --origin https://seller.example
23
+ envarpay serve --config /private/envarpay/seller.json --credentials /private/envarpay/credentials.json --state /private/envarpay/seller.sqlite3 --origin https://seller.example --envar-config /private/envarpay/envar.json --envar-credentials /private/envarpay/envar.token
24
+ envarpay inspect --state /private/envarpay/seller.sqlite3
25
+ ```
64
26
 
65
- ## Payment guarantees
27
+ `init` creates a sample seller config and vault key; it does not authorize paid work. Put secrets only in owner-only files (`chmod 600`), keep the state directory private, and use one running process per ledger. The runtime verifies the configured service revision, original A2A Task and one captured Envar balance claim before calling the private Agent. It stores the original claim and Task handles so a lost response can be recovered without charging twice.
66
28
 
67
- Payment confirmation, task completion and acceptance are separate facts. The MVP
68
- charges upfront for a new task; reads and permitted clarification use that same
69
- purchase. It does not provide escrow, automatic refunds, subscriptions or billing
70
- for arbitrary internal tools. A2A compatibility alone does not give an Agent a
71
- wallet or authorize payment.
29
+ The seller configuration schema is [`schemas/envarpay.config.schema.json`](schemas/envarpay.config.schema.json). Paid amounts are integer **micro-USD** (`100000` = USD 0.10); free offers have no payment profile. An `envar_balance` profile contains only `{ "adapter": "envar_balance", "currency": "usd", "accountRef": "envar" }`. The private Envar policy must allow that exact account reference.
72
30
 
73
- MPP requires an eligible Stripe merchant, a permitted buyer token-creation flow,
74
- explicit test/live mode and any required customer authentication. Simulator tests
75
- are not card charges. Provider and chain acceptance must be recorded separately
76
- from SDK tests and build results.
31
+ For development, run `npm ci --ignore-scripts`, `npm run build`, and `npm test` from this directory. Node.js 22.14+ is required; Node 24 is recommended.
@@ -1,31 +1,21 @@
1
- type SignalName = 'ledger_read' | 'payment_unknown' | 'execution_unknown' | 'recorded_provider_failure' | 'budget_near_limit' | 'budget_exhausted' | 'report_backlog' | 'report_failure';
1
+ type Name = 'ledger_read' | 'payment_unknown' | 'execution_unknown' | 'report_backlog' | 'report_failure';
2
2
  type Signal = {
3
- name: SignalName;
3
+ name: Name;
4
4
  status: 'clear' | 'alert' | 'unavailable' | 'not_applicable';
5
5
  severity: 'info' | 'warning' | 'critical';
6
6
  count: number | null;
7
7
  };
8
- type Budget = {
9
- unit: string;
10
- maxTotal: string;
11
- reserved: string;
12
- spent: string;
13
- remaining: string;
14
- state: 'available' | 'near_limit' | 'exhausted';
15
- };
16
8
  export interface LocalAlertOptions {
17
- policyPath?: string;
18
9
  integrationPath?: string;
19
10
  integrationDisabled?: boolean;
20
11
  }
21
- /** Owner-local aggregate evidence only. Never opens a runtime store, signer, vault or network. */
22
12
  export declare function inspectLocalAlerts(path: string, options?: LocalAlertOptions): {
23
- role: "unavailable" | "buyer" | "seller";
13
+ role: "unavailable" | "seller";
24
14
  view: "alerts";
25
15
  observation: "local_persisted_state_only";
26
- status: string;
16
+ status: "unavailable" | "partial" | "available";
27
17
  signals: Signal[];
28
- budgets: Budget[];
18
+ budgets: never[];
29
19
  alertCount: number;
30
20
  unavailableCount: number;
31
21
  providerHealth: "unavailable_not_probed";
@@ -1,18 +1,10 @@
1
+ /** Read-only health signals for one seller ledger and its Envar report queue. */
1
2
  import { DatabaseSync } from 'node:sqlite';
2
- import { lstatSync, readFileSync } from 'node:fs';
3
- import { loadBuyerPolicy } from './config.js';
4
- const CURRENCIES = {
5
- usd: 'usd_cents',
6
- 'eip155:8453/erc20:0x833589fcd6edb6e08f4c7c32d4f71b54bda02913': 'base_usdc_atomic',
7
- 'eip155:84532/erc20:0x036cbd53842c5426634e7929541ec2318f3dcf7e': 'base_sepolia_usdc_atomic',
8
- };
9
- function privateFile(path, limit) {
3
+ import { lstatSync } from 'node:fs';
4
+ function database(path) {
10
5
  const stat = lstatSync(path);
11
- if (!stat.isFile() || stat.isSymbolicLink() || (stat.mode & 0o077) !== 0 || (process.getuid && stat.uid !== process.getuid()) || (limit !== undefined && stat.size > limit))
6
+ if (!stat.isFile() || stat.isSymbolicLink() || (stat.mode & 0o077) !== 0 || (process.getuid && stat.uid !== process.getuid()))
12
7
  throw new Error('private_source_unavailable');
13
- }
14
- function database(path) {
15
- privateFile(path);
16
8
  const db = new DatabaseSync(path, { readOnly: true });
17
9
  try {
18
10
  db.exec('PRAGMA query_only=ON; PRAGMA busy_timeout=1000; BEGIN');
@@ -27,61 +19,19 @@ const count = (db, sql) => Number(db.prepare(sql).get().n);
27
19
  const signal = (name, value, severity = 'warning') => ({ name, status: value ? 'alert' : 'clear', severity: value ? severity : 'info', count: value });
28
20
  const missing = (name) => ({ name, status: 'unavailable', severity: 'warning', count: null });
29
21
  const irrelevant = (name) => ({ name, status: 'not_applicable', severity: 'info', count: null });
30
- /** Owner-local aggregate evidence only. Never opens a runtime store, signer, vault or network. */
31
22
  export function inspectLocalAlerts(path, options = {}) {
32
- const signals = [], budgets = [];
33
- let role = 'unavailable', db;
23
+ const signals = [];
24
+ let db;
25
+ let available = false;
34
26
  try {
35
27
  db = database(path);
36
- const tables = new Set(db.prepare("SELECT name FROM sqlite_master WHERE type='table'").all().map(row => row.name));
37
- if (tables.has('buyer_purchases') === tables.has('commerce_orders'))
38
- throw new Error('unknown_ledger');
39
- role = tables.has('buyer_purchases') ? 'buyer' : 'seller';
40
- if (db.prepare(`SELECT version FROM ${role === 'buyer' ? 'buyer_meta' : 'commerce_meta'}`).get()?.version !== (role === 'buyer' ? 1 : 2))
28
+ if (db.prepare("SELECT version FROM commerce_meta").get()?.version !== 2)
41
29
  throw new Error('unknown_version');
42
- const unknownPayments = count(db, role === 'buyer' ? "SELECT COUNT(*) n FROM buyer_purchases WHERE json_extract(record_json,'$.paymentState')='unknown'" : "SELECT COUNT(*) n FROM commerce_orders WHERE payment_state='unknown'");
43
- const unknownExecution = count(db, role === 'buyer' ? "SELECT COUNT(*) n FROM buyer_purchases WHERE json_extract(record_json,'$.executionState')='unknown'" : "SELECT COUNT(*) n FROM commerce_orders WHERE execution_state='unknown'");
44
- // Only concrete persisted provider errors count. Generic unknown is not proof of provider failure.
45
- const providerFailures = count(db, role === 'buyer'
46
- ? "SELECT COUNT(*) n FROM buyer_purchases WHERE json_extract(record_json,'$.paymentState')!='confirmed' AND json_extract(record_json,'$.errorCode') IN ('stripe_transport_unknown','stripe_request_failed','stripe_rate_limit','stripe_response','stripe_response_size')"
47
- : "SELECT COUNT(*) n FROM commerce_payment_attempts WHERE state!='confirmed' AND json_extract(receipt_json,'$.success')=0 AND length(json_extract(receipt_json,'$.errorReason'))>0");
48
- signals.push(signal('ledger_read', 0), signal('payment_unknown', unknownPayments), signal('execution_unknown', unknownExecution), signal('recorded_provider_failure', providerFailures));
49
- if (role === 'seller')
50
- signals.push(irrelevant('budget_near_limit'), irrelevant('budget_exhausted'));
51
- else {
52
- try {
53
- if (!options.policyPath)
54
- throw new Error('policy_required');
55
- privateFile(options.policyPath, 1024 * 1024);
56
- const policy = loadBuyerPolicy(JSON.parse(readFileSync(options.policyPath, 'utf8')));
57
- const usage = new Map();
58
- for (const b of policy.budgets)
59
- usage.set(b.currency, { reserved: 0n, spent: 0n });
60
- for (const row of db.prepare("SELECT currency,amount,budget_state FROM buyer_purchases WHERE budget_state!='none'").iterate()) {
61
- const currency = String(row.currency), amount = String(row.amount), state = row.budget_state;
62
- const target = usage.get(currency);
63
- if (!target || !/^[1-9][0-9]{0,77}$/.test(amount) || !['spent', 'reserved'].includes(String(state)))
64
- throw new Error('budget_binding');
65
- target[state === 'spent' ? 'spent' : 'reserved'] += BigInt(amount);
66
- }
67
- const checked = policy.budgets.map(b => {
68
- const u = usage.get(b.currency), cap = BigInt(b.maxTotal), used = u.reserved + u.spent;
69
- const state = used >= cap ? 'exhausted' : used * 10n >= cap * 9n ? 'near_limit' : 'available';
70
- return { unit: CURRENCIES[b.currency], maxTotal: cap.toString(), reserved: u.reserved.toString(), spent: u.spent.toString(), remaining: (used < cap ? cap - used : 0n).toString(), state };
71
- });
72
- budgets.push(...checked);
73
- signals.push(signal('budget_near_limit', checked.filter(b => b.state === 'near_limit').length), signal('budget_exhausted', checked.filter(b => b.state === 'exhausted').length, 'critical'));
74
- }
75
- catch {
76
- signals.push(missing('budget_near_limit'), missing('budget_exhausted'));
77
- }
78
- }
30
+ signals.push(signal('ledger_read', 0), signal('payment_unknown', count(db, "SELECT COUNT(*) n FROM commerce_orders WHERE payment_state='unknown'"), 'critical'), signal('execution_unknown', count(db, "SELECT COUNT(*) n FROM commerce_orders WHERE execution_state='unknown'")));
31
+ available = true;
79
32
  }
80
33
  catch {
81
- role = 'unavailable';
82
- signals.length = 0;
83
- budgets.length = 0;
84
- signals.push(...['ledger_read', 'payment_unknown', 'execution_unknown', 'recorded_provider_failure', 'budget_near_limit', 'budget_exhausted'].map(missing));
34
+ signals.push(missing('ledger_read'), missing('payment_unknown'), missing('execution_unknown'));
85
35
  }
86
36
  finally {
87
37
  db?.close();
@@ -92,21 +42,12 @@ export function inspectLocalAlerts(path, options = {}) {
92
42
  signals.push(irrelevant('report_backlog'), irrelevant('report_failure'));
93
43
  }
94
44
  else {
95
- if (options.integrationDisabled)
96
- throw new Error('ambiguous_integration');
97
- if (!options.integrationPath)
45
+ if (options.integrationDisabled || !options.integrationPath)
98
46
  throw new Error('integration_not_supplied');
99
47
  integration = database(options.integrationPath);
100
- const binding = integration.prepare("SELECT value FROM meta WHERE key='binding'").get();
101
- const source = integration.prepare("SELECT value FROM meta WHERE key='source_instance'").get();
102
- if (!binding || !source || typeof binding.value !== 'string' || typeof source.value !== 'string')
103
- throw new Error('integration_schema');
104
- const parsed = JSON.parse(binding.value);
105
- if (!parsed || typeof parsed.platformOrigin !== 'string' || typeof parsed.agentId !== 'string' || typeof parsed.runtimeAgentId !== 'string')
48
+ if (!integration.prepare("SELECT value FROM meta WHERE key='binding'").get() || !integration.prepare("SELECT value FROM meta WHERE key='source_instance'").get())
106
49
  throw new Error('integration_schema');
107
- const pending = count(integration, "SELECT COUNT(*) n FROM outbox WHERE state='pending'");
108
- const failed = count(integration, "SELECT COUNT(*) n FROM outbox WHERE state='pending' AND attempts>0 AND last_error<>''");
109
- signals.push(signal('report_backlog', pending), signal('report_failure', failed));
50
+ signals.push(signal('report_backlog', count(integration, "SELECT COUNT(*) n FROM outbox WHERE state='pending'")), signal('report_failure', count(integration, "SELECT COUNT(*) n FROM outbox WHERE state='pending' AND attempts>0 AND last_error<>''")));
110
51
  }
111
52
  }
112
53
  catch {
@@ -115,11 +56,14 @@ export function inspectLocalAlerts(path, options = {}) {
115
56
  finally {
116
57
  integration?.close();
117
58
  }
118
- const alertCount = signals.filter(s => s.status === 'alert').length, unavailableCount = signals.filter(s => s.status === 'unavailable').length;
59
+ const alertCount = signals.filter(s => s.status === 'alert').length;
60
+ const unavailableCount = signals.filter(s => s.status === 'unavailable').length;
119
61
  return {
120
- role, view: 'alerts', observation: 'local_persisted_state_only',
121
- status: role === 'unavailable' ? 'unavailable' : unavailableCount ? 'partial' : 'available',
122
- signals, budgets, alertCount, unavailableCount,
62
+ role: available ? 'seller' : 'unavailable',
63
+ view: 'alerts',
64
+ observation: 'local_persisted_state_only',
65
+ status: !available ? 'unavailable' : unavailableCount ? 'partial' : 'available',
66
+ signals, budgets: [], alertCount, unavailableCount,
123
67
  providerHealth: 'unavailable_not_probed',
124
68
  recommendedExitCode: unavailableCount ? 3 : alertCount ? 2 : 0,
125
69
  paymentPerformed: false, refundPerformed: false, networkRequests: 0,
@@ -14,7 +14,7 @@ export function createOfferCard(config, serviceId, offerId, origin) {
14
14
  if (!service || !offer)
15
15
  throw new CommerceError('offer_not_found', 'Service offer not found');
16
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 === 'envar_balance' ? { adapter: 'envar_balance', currency: 'usd', accountRef: profile.accountRef } : profile ? { adapter: 'mpp', method: 'stripe', intent: 'charge', currency: profile.currency } : null;
17
+ const payment = profile ? { adapter: 'envar_balance', currency: 'usd', accountRef: profile.accountRef } : null;
18
18
  return AgentCard.fromJSON({
19
19
  name: service.name,
20
20
  description: `Deliverables: ${service.contract.deliverables.join(', ')}`,
@@ -5,10 +5,8 @@ import { createRequire } from 'node:module';
5
5
  import { randomBytes } from 'node:crypto';
6
6
  import { once } from 'node:events';
7
7
  import { dirname, join, resolve } from 'node:path';
8
- import { pathToFileURL } from 'node:url';
9
- import { privateKeyToAccount } from 'viem/accounts';
10
8
  import { AgentCard } from '@a2a-js/sdk';
11
- import { loadCommerceConfig, loadBuyerPolicy, digest } from './config.js';
9
+ import { loadCommerceConfig, digest } from './config.js';
12
10
  import { createOfferCard } from './card.js';
13
11
  import { CommerceStore } from './store.js';
14
12
  import { CommerceServer, bearerAuthenticator } from './server.js';
@@ -16,16 +14,14 @@ import { EnvarIntegration } from './envar.js';
16
14
  import { EnvarSellerRuntime, PinnedUpstreams, restoreEnvarConfig } from './envar-runtime.js';
17
15
  import { listenCommerce } from './http.js';
18
16
  import { CredentialVault } from './vault.js';
19
- import { X402Gate } from './x402.js';
20
17
  import { EnvarBalanceGate } from './envar-balance.js';
21
- import { evmReceiptVerifier, evmSettlementRecovery } from './chain.js';
22
18
  import { CommerceError } from './types.js';
23
19
  import { createNativeSkillExecutor } from './native-skills.js';
24
20
  import { inspectLedger, inspectLocalAlerts } from './operations.js';
25
21
  import { serveManaged } from './managed.js';
26
22
  import { setupAgent } from './setup.js';
27
23
  import { SkillDiscovery } from './discovery.js';
28
- import { upgradeDiscovery, configurePayments } from './configure.js';
24
+ import { upgradeDiscovery } from './configure.js';
29
25
  import { agentSetup } from './agent-setup.js';
30
26
  import { setupExternalAgent, serveExternalAgent } from './external-runtime.js';
31
27
  const { values, positionals } = parseArgs({ allowPositionals: true, options: {
@@ -35,7 +31,7 @@ const { values, positionals } = parseArgs({ allowPositionals: true, options: {
35
31
  after: { type: 'string' }, limit: { type: 'string' }, 'refund-review': { type: 'boolean' }, alerts: { type: 'boolean' }, 'fail-on-alert': { type: 'boolean' }, 'integration-state': { type: 'string' }, 'no-integration': { type: 'boolean' },
36
32
  framework: { type: 'string' }, file: { type: 'string' }, 'docker-container': { type: 'string' }, python: { type: 'string' }, 'hermes-home': { type: 'string' }, check: { type: 'boolean' },
37
33
  } });
38
- const usage = 'envarpay external-setup --directory DIR --origin HTTPS_ORIGIN | external-serve --config FILE --credentials FILE --state DB --origin HTTPS_ORIGIN\nenvarpay onboard --file invitation.json [--framework hermes|openclaw] [--check] [--python PATH] [--docker-container NAME] [--hermes-home PATH] [--origin HTTPS_ORIGIN]\nenvarpay upgrade --directory DIR [--hermes-home PATH] | payments --directory DIR --file receiving.json\nenvarpay setup --file envar-setup.json [--docker-container NAME | --python PATH] [--hermes-home PATH] [--directory DIR]\ninit --directory DIR | inspect --state DB [--after ROW --limit 100 --refund-review] [--alerts --config buyer-policy.json --integration-state DB/--no-integration --fail-on-alert] | 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';
34
+ const usage = 'envarpay setup --file envar-setup.json [--docker-container NAME | --python PATH] [--hermes-home PATH] [--directory DIR]\nenvarpay onboard --file invitation.json [--framework hermes|openclaw] [--check] [--python PATH] [--docker-container NAME] [--hermes-home PATH] [--origin HTTPS_ORIGIN]\nenvarpay upgrade --directory DIR [--hermes-home PATH]\nenvarpay external-setup --directory DIR --origin HTTPS_ORIGIN | external-serve --config FILE --credentials FILE --state DB --origin HTTPS_ORIGIN\nenvarpay init --directory DIR | inspect --state DB [--after ROW --limit 100 --refund-review] | validate --config seller.json | card --config seller.json --service ID --offer ID --origin URL | serve --config FILE --credentials FILE --state DB --origin URL';
39
35
  const closers = [];
40
36
  function privateFile(path) {
41
37
  const stat = lstatSync(path);
@@ -75,16 +71,6 @@ function signals() {
75
71
  async function closeHttp(http) {
76
72
  await new Promise((resolve, reject) => http.close(error => error ? reject(error) : resolve()));
77
73
  }
78
- async function loadMppAdapter(path) {
79
- privateFile(path);
80
- const module = await import(pathToFileURL(resolve(path)).href);
81
- if (typeof module.createMppBuyerOptions !== 'function')
82
- throw new CommerceError('mpp_adapter_module', 'Owner-supplied module must export createMppBuyerOptions');
83
- const options = await module.createMppBuyerOptions();
84
- if (!options || typeof options.createToken !== 'function' || typeof options.recoverToken !== 'function' || typeof options.verifyReceipt !== 'function')
85
- throw new CommerceError('mpp_adapter_module', 'MPP requires real token creation, original-operation recovery and receipt verification');
86
- return options;
87
- }
88
74
  async function run() {
89
75
  if (values.help || positionals.length === 0 && !values.version) {
90
76
  console.log(usage);
@@ -129,10 +115,6 @@ async function run() {
129
115
  upgradeDiscovery(values.directory, values['hermes-home']);
130
116
  return;
131
117
  }
132
- if (command === 'payments' && values.directory && values.file) {
133
- await configurePayments(values.directory, values.file);
134
- return;
135
- }
136
118
  if (command === 'setup') {
137
119
  if (!values.file)
138
120
  throw new CommerceError('setup_file', 'Download the setup file from your Agent service page and pass --file PATH');
@@ -142,7 +124,7 @@ async function run() {
142
124
  if (values.alerts || values['fail-on-alert'] || values['integration-state'] || values['no-integration']) {
143
125
  if (command !== 'inspect' || !values.state || !values.alerts || values['refund-review'] || values.after || values.limit || (values['no-integration'] && values['integration-state']))
144
126
  throw new CommerceError('inspection_view', 'Alert inspection needs inspect --state and --alerts, without pagination or refund review; choose at most one integration source');
145
- const report = inspectLocalAlerts(values.state, { policyPath: values.config, integrationPath: values['integration-state'], integrationDisabled: values['no-integration'] });
127
+ const report = inspectLocalAlerts(values.state, { integrationPath: values['integration-state'], integrationDisabled: values['no-integration'] });
146
128
  console.log(JSON.stringify(report, null, 2));
147
129
  if (values['fail-on-alert'])
148
130
  process.exitCode = report.recommendedExitCode;
@@ -160,7 +142,7 @@ async function run() {
160
142
  throw new CommerceError('envar_configuration', 'Supply both --envar-config and --envar-credentials');
161
143
  if (command === 'init' && values.directory) {
162
144
  mkdirSync(values.directory, { recursive: true, mode: 0o700 });
163
- for (const [name, source] of [['seller.json', 'seller.json'], ['buyer-policy.json', 'buyer-policy.json']]) {
145
+ for (const [name, source] of [['seller.json', 'seller.json']]) {
164
146
  writeFileSync(join(values.directory, name), readFileSync(new URL(`../../examples/${source}`, import.meta.url)), { flag: 'wx', mode: 0o600 });
165
147
  }
166
148
  writeFileSync(join(values.directory, 'vault.key'), randomBytes(32), { flag: 'wx', mode: 0o600, flush: true });
@@ -170,81 +152,6 @@ async function run() {
170
152
  if (!values.config)
171
153
  throw new CommerceError('usage', usage);
172
154
  const raw = JSON.parse(readFileSync(values.config, 'utf8'));
173
- if (command === 'buyer-serve') {
174
- if (!values.origin || !values.state || !values.credentials)
175
- throw new CommerceError('usage', usage);
176
- const policy = loadBuyerPolicy(raw);
177
- const secrets = JSON.parse(privateFile(values.credentials).toString('utf8'));
178
- const needsEvm = policy.peers.some(peer => peer.protocol === 'x402'), needsMpp = policy.peers.some(peer => peer.protocol === 'mpp');
179
- if (!secrets.peerTokens || (needsEvm || needsMpp) && !secrets.vaultKeyFile)
180
- throw new CommerceError('buyer_credentials', 'Paid buyer needs private vault key and peer credentials');
181
- let signer;
182
- if (needsEvm) {
183
- if (!secrets.privateKeyFile || !secrets.rpcUrls)
184
- throw new CommerceError('buyer_credentials', 'x402 peers require local signer and RPC configuration');
185
- const key = privateFile(secrets.privateKeyFile).toString('utf8').trim();
186
- if (!/^0x[0-9a-fA-F]{64}$/.test(key))
187
- throw new CommerceError('buyer_key', 'Signer file must contain one 32-byte 0x-prefixed private key');
188
- signer = privateKeyToAccount(key);
189
- }
190
- if (secrets.mppAdapterModule && secrets.mppStripe)
191
- throw new CommerceError('mpp_configuration', 'Choose the built-in Stripe issuer or one explicit adapter module');
192
- if (needsMpp && !secrets.mppAdapterModule && !secrets.mppStripe)
193
- throw new CommerceError('mpp_configuration', 'MPP peers require a configured Stripe issuer or explicit authorized funding adapter');
194
- const buyerVault = secrets.vaultKeyFile ? new CredentialVault(join(dirname(values.state), 'buyer-authorizations'), privateFile(secrets.vaultKeyFile)) : undefined;
195
- let mpp;
196
- if (needsMpp && secrets.mppStripe) {
197
- const configured = secrets.mppStripe;
198
- if (!configured.secretKeyFile || !configured.sellers || typeof configured.sellers !== 'object' || Array.isArray(configured.sellers))
199
- throw new CommerceError('mpp_configuration', 'Stripe issuer and seller read credentials must use private key files');
200
- const { createStripeMppBuyer } = await import('./stripe-issuer.js');
201
- const adapter = createStripeMppBuyer({
202
- payer: configured.payer, mode: configured.mode, issuerAccountId: configured.issuerAccountId,
203
- secretKey: privateFile(configured.secretKeyFile).toString('utf8').trim(), paymentMethod: configured.paymentMethod,
204
- ...(configured.publishableKey !== undefined ? { publishableKey: configured.publishableKey } : {}),
205
- sellers: Object.fromEntries(Object.entries(configured.sellers).map(([id, seller]) => [id, { accountId: seller.accountId, secretKey: privateFile(seller.secretKeyFile).toString('utf8').trim() }])),
206
- ...(configured.issuance ? { issuance: configured.issuance } : {}), ...(configured.returnUrl ? { returnUrl: configured.returnUrl } : {}),
207
- }, { statePath: values.state + '.stripe-issuer.sqlite3', vault: buyerVault });
208
- closers.push(() => adapter.close());
209
- await adapter.assertReady();
210
- mpp = adapter;
211
- }
212
- else if (needsMpp)
213
- mpp = await loadMppAdapter(secrets.mppAdapterModule);
214
- const { BuyerStore } = await import('./buyer-store.js');
215
- const { CommerceBuyer } = await import('./buyer.js');
216
- const { BuyerManagement, listenBuyerManagement } = await import('./management.js');
217
- const store = new BuyerStore(values.state);
218
- closers.push(() => store.close());
219
- const buyer = new CommerceBuyer({ policy, store, vault: buyerVault,
220
- signer, mpp, peerTokens: secrets.peerTokens, peerEndpoints: secrets.peerEndpoints,
221
- ...(needsEvm ? { verifyReceipt: evmReceiptVerifier(secrets.rpcUrls), ...evmSettlementRecovery(secrets.rpcUrls) } : {}) });
222
- closers.push(() => buyer.stop());
223
- if (secrets.peerProxies?.length) {
224
- if (secrets.peerProxies.length > 8)
225
- throw new CommerceError('proxy_limit', 'Configure at most8 explicit peer proxies per buyer');
226
- const { A2APeerProxy, listenPeerProxy } = await import('./peer-proxy.js');
227
- for (const settings of secrets.peerProxies) {
228
- if (Object.keys(settings.tokens).some(token => Object.hasOwn(secrets.callers, token)))
229
- throw new CommerceError('proxy_token_scope', 'Agent proxy tokens must differ from owner management credentials');
230
- if (!Number.isInteger(settings.port) || settings.port < 1 || settings.port > 65535)
231
- throw new CommerceError('proxy_port', 'Invalid proxy listen port');
232
- const proxy = new A2APeerProxy({ ...settings, buyer });
233
- closers.push(async () => { await proxy.stop(); proxy.close(); });
234
- const listener = listenPeerProxy(proxy, settings.host ?? '127.0.0.1', settings.port);
235
- closers.push(() => closeHttp(listener));
236
- await once(listener, 'listening');
237
- }
238
- }
239
- const management = new BuyerManagement({ buyer, origin: values.origin, authenticate: bearerAuthenticator(secrets.callers) });
240
- const http = listenBuyerManagement(management, values.origin, values.host ?? '127.0.0.1', port(4021));
241
- closers.push(() => closeHttp(http));
242
- await once(http, 'listening');
243
- buyer.start();
244
- signals();
245
- console.log(JSON.stringify({ listening: port(4021), role: 'buyer', paymentsEnabled: policy.paymentsEnabled }));
246
- return;
247
- }
248
155
  let config = loadCommerceConfig(raw);
249
156
  if (command === 'validate') {
250
157
  console.log(JSON.stringify({ valid: true, digest: digest(config), services: config.services.length, paymentPerformed: false }));
@@ -276,46 +183,18 @@ async function run() {
276
183
  closers.push(() => store.close());
277
184
  if (integration)
278
185
  config = restoreEnvarConfig(config, integration, store.catalogHistory());
279
- const protocols = new Set(Object.values(config.paymentProfiles).map(p => p.adapter));
280
- for (const historical of store.catalogHistory())
281
- for (const profile of Object.values(historical.profiles))
282
- protocols.add(profile.adapter);
283
- if (syncSettings?.policy.allowedX402.length)
284
- protocols.add('x402');
285
- if (syncSettings?.policy.allowedMppAccounts.length)
286
- protocols.add('mpp');
186
+ const needsBalance = Boolean(integration) || Object.values(config.paymentProfiles).length > 0 || store.catalogHistory().some(entry => Object.values(entry.profiles).length > 0);
287
187
  let vault;
288
- if (protocols.size) {
188
+ if (needsBalance) {
289
189
  if (!secrets.vaultKeyFile)
290
190
  throw new CommerceError('vault_key', 'Paid services require a private vault key file');
291
191
  vault = new CredentialVault(join(dirname(values.state), 'authorizations'), privateFile(secrets.vaultKeyFile));
292
192
  }
293
- const gates = {};
294
- let stripeRecipientFor;
295
- if (protocols.has('x402')) {
296
- if (!secrets.payers || !secrets.rpcUrls)
297
- throw new CommerceError('x402_configuration', 'x402 requires explicit caller-payer bindings and network RPCs');
298
- const gate = new X402Gate({ store, vault: vault, payerFor: caller => secrets.payers?.[caller], verifyReceipt: evmReceiptVerifier(secrets.rpcUrls), ...evmSettlementRecovery(secrets.rpcUrls) });
299
- await gate.check([...Object.values(config.paymentProfiles).filter((p) => p.adapter === 'x402'), ...(secrets.receivingProfiles ?? [])]);
300
- gates.x402 = gate;
301
- }
302
- if (protocols.has('mpp')) {
303
- if (!secrets.stripe || !secrets.mppHmacKeyFile)
304
- throw new CommerceError('mpp_configuration', 'MPP requires private Stripe credentials and a separate challenge key');
305
- const { MppGate } = await import('./mpp.js'), { stripeProvider } = await import('./stripe-provider.js');
306
- const providers = Object.fromEntries(Object.entries(secrets.stripe).map(([ref, p]) => [ref, stripeProvider({ ...p, secretKey: privateFile(p.secretKeyFile).toString('utf8').trim() })]));
307
- const hmacKey = privateFile(secrets.mppHmacKeyFile);
308
- if (hmacKey.length < 32)
309
- throw new CommerceError('mpp_hmac_key', 'MPP requires a private challenge key of at least32 random bytes');
310
- const gate = new MppGate({ store, vault: vault, origin: values.origin, hmacSecret: hmacKey.toString('base64'), providers });
311
- await gate.check();
312
- gates.mpp = gate;
313
- stripeRecipientFor = ref => gate.merchantRecipient(ref);
314
- }
315
- if (protocols.has('envar_balance')) {
193
+ let balanceGate;
194
+ if (needsBalance) {
316
195
  if (!integration)
317
196
  throw new CommerceError('balance_configuration', 'Envar balance requires an authenticated Envar seller integration');
318
- gates.envar_balance = new EnvarBalanceGate({ store, vault: vault, claim: args => integration.claimBalancePayment(args) });
197
+ balanceGate = new EnvarBalanceGate({ store, vault: vault, claim: args => integration.claimBalancePayment(args) });
319
198
  }
320
199
  const nativeSkills = secrets.skills ? createNativeSkillExecutor(secrets.skills, values.origin, config.agent.name) : undefined;
321
200
  const discovery = secrets.skills?.discovery?.enabledFromWeb ? new SkillDiscovery(secrets.skills, secrets.skills.discovery, values.origin) : undefined;
@@ -325,30 +204,23 @@ async function run() {
325
204
  upstreams.prepare(config, store.catalogHistory().map(entry => entry.service));
326
205
  const validateAdapters = (candidate) => {
327
206
  for (const profile of Object.values(candidate.paymentProfiles)) {
328
- if (!gates[profile.adapter])
329
- throw new CommerceError('payment_method_unavailable', 'Configure the payment adapter locally before enabling this service');
330
- if (profile.adapter === 'x402' && !secrets.rpcUrls?.[profile.network])
331
- throw new CommerceError('x402_configuration', 'Configure the required network RPC locally');
332
- if (profile.adapter === 'mpp' && !stripeRecipientFor?.(profile.accountRef))
333
- throw new CommerceError('mpp_configuration', 'Configure and verify the merchant account locally');
334
- if (profile.adapter === 'envar_balance' && !syncSettings?.policy.allowedBalanceAccounts?.includes(profile.accountRef))
207
+ if (!balanceGate || !syncSettings?.policy.allowedBalanceAccounts?.includes(profile.accountRef))
335
208
  throw new CommerceError('balance_configuration', 'Allow this balance receiving account locally');
336
209
  }
337
210
  };
338
211
  validateAdapters(config);
339
- const server = new CommerceServer({ config, origin: values.origin, store, stripeRecipientFor, ownershipChallenges: secrets.ownershipChallenges,
212
+ const server = new CommerceServer({ config, origin: values.origin, store, ownershipChallenges: secrets.ownershipChallenges,
340
213
  authenticate: bearerAuthenticator(secrets.callers), execute: upstreams.execute,
341
214
  paymentGate: { handle: async (request, order) => {
342
- const adapter = order.quote.paymentProfile?.adapter, gate = adapter ? gates[adapter] : undefined;
343
- if (!gate)
344
- throw new CommerceError('payment_method_unavailable', 'Configured payment adapter is unavailable');
345
- return gate.handle(request, order);
215
+ if (!balanceGate)
216
+ throw new CommerceError('payment_method_unavailable', 'Envar balance is unavailable');
217
+ return balanceGate.handle(request, order);
346
218
  } }, onError: (code, orderId) => console.error(JSON.stringify({ code, orderId })) });
347
219
  closers.push(async () => { server.stop(); controller.abort(); while (server.isRunning)
348
220
  await new Promise(r => setTimeout(r, 50)); });
349
221
  let connection;
350
222
  if (integration) {
351
- connection = new EnvarSellerRuntime({ integration, server, upstreams, pollIntervalMs: syncSettings?.pollIntervalMs, validateConfig: validateAdapters, beforeSync: discovery ? () => discovery.sync(integration, [...Object.values(server.config.paymentProfiles).filter(p => p.adapter === 'x402'), ...(secrets.receivingProfiles ?? [])].map(p => ({ network: p.network, pay_to: p.payTo, facilitator_url: p.facilitatorUrl }))) : undefined, onError: code => console.error(JSON.stringify({ code, integration: 'envar' })) });
223
+ connection = new EnvarSellerRuntime({ integration, server, upstreams, pollIntervalMs: syncSettings?.pollIntervalMs, validateConfig: validateAdapters, beforeSync: discovery ? () => discovery.sync(integration) : undefined, onError: code => console.error(JSON.stringify({ code, integration: 'envar' })) });
352
224
  closers.push(() => connection.stop());
353
225
  }
354
226
  const http = listenCommerce(server, values.origin, values.host ?? '127.0.0.1', port(4020));
@@ -356,7 +228,7 @@ async function run() {
356
228
  await once(http, 'listening');
357
229
  connection?.start();
358
230
  signals();
359
- console.log(JSON.stringify({ listening: port(4020), role: 'seller', paidTransports: [...protocols] }));
231
+ console.log(JSON.stringify({ listening: port(4020), role: 'seller', paymentMethod: needsBalance ? 'envar_balance' : 'free' }));
360
232
  }
361
233
  try {
362
234
  await run();
@@ -1,11 +1,8 @@
1
- import { type BuyerPolicy, type CommerceConfig, type Offer, type PaymentProfile, type Service } from './types.js';
1
+ import { type CommerceConfig, type Offer, type PaymentProfile, type Service } from './types.js';
2
2
  export declare function atomic(value: string): bigint;
3
3
  export declare function validateUrl(value: string, allowPrivate?: boolean): URL;
4
4
  export declare function currencyOf(profile: PaymentProfile): string;
5
- /** Stripe documents sandbox Business Profile IDs as profile_test_<opaque id>. */
6
- export declare function isStripeProfileId(value: unknown): value is string;
7
5
  export declare function digest(value: unknown): string;
8
6
  /** Keep legacy quotes unchanged; skill-bound quotes freeze the installed skill revision too. */
9
7
  export declare function serviceTermsDigest(configVersion: 1 | 2, service: Service, offer: Offer, profile: PaymentProfile | null): string;
10
8
  export declare function loadCommerceConfig(value: unknown): CommerceConfig;
11
- export declare function loadBuyerPolicy(value: unknown): BuyerPolicy;