@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.
- package/README.md +18 -63
- package/dist/commerce/alerts.d.ts +5 -15
- package/dist/commerce/alerts.js +21 -77
- package/dist/commerce/card.js +1 -1
- package/dist/commerce/cli.js +17 -145
- package/dist/commerce/config.d.ts +1 -4
- package/dist/commerce/config.js +4 -82
- package/dist/commerce/configure.d.ts +0 -1
- package/dist/commerce/configure.js +0 -80
- package/dist/commerce/discovery.d.ts +1 -5
- package/dist/commerce/discovery.js +1 -4
- package/dist/commerce/envar-balance.js +1 -1
- package/dist/commerce/envar.d.ts +1 -32
- package/dist/commerce/envar.js +3 -82
- package/dist/commerce/operations.d.ts +1 -2
- package/dist/commerce/operations.js +17 -20
- package/dist/commerce/pricing.d.ts +0 -1
- package/dist/commerce/pricing.js +1 -3
- package/dist/commerce/runtime.d.ts +0 -2
- package/dist/commerce/runtime.js +0 -2
- package/dist/commerce/server.d.ts +0 -1
- package/dist/commerce/server.js +2 -3
- package/dist/commerce/setup.js +4 -3
- package/dist/commerce/types.d.ts +0 -59
- package/examples/envar-integration-policy.json +1 -9
- package/examples/seller.json +143 -26
- package/package.json +3 -16
- package/schemas/envarpay.config.schema.json +1 -80
- package/dist/commerce/buyer-store.d.ts +0 -101
- package/dist/commerce/buyer-store.js +0 -168
- package/dist/commerce/buyer.d.ts +0 -138
- package/dist/commerce/buyer.js +0 -775
- package/dist/commerce/chain.d.ts +0 -24
- package/dist/commerce/chain.js +0 -196
- package/dist/commerce/client-runtime.d.ts +0 -8
- package/dist/commerce/client-runtime.js +0 -7
- package/dist/commerce/management.d.ts +0 -14
- package/dist/commerce/management.js +0 -140
- package/dist/commerce/mpp-client.d.ts +0 -70
- package/dist/commerce/mpp-client.js +0 -29
- package/dist/commerce/mpp-runtime.d.ts +0 -4
- package/dist/commerce/mpp-runtime.js +0 -4
- package/dist/commerce/mpp.d.ts +0 -53
- package/dist/commerce/mpp.js +0 -163
- package/dist/commerce/peer-proxy.d.ts +0 -44
- package/dist/commerce/peer-proxy.js +0 -673
- package/dist/commerce/stripe-issuer.d.ts +0 -41
- package/dist/commerce/stripe-issuer.js +0 -351
- package/dist/commerce/stripe-provider.d.ts +0 -11
- package/dist/commerce/stripe-provider.js +0 -132
- package/dist/commerce/x402.d.ts +0 -38
- package/dist/commerce/x402.js +0 -181
- package/examples/buyer-policy.json +0 -18
- package/schemas/buyer-policy.schema.json +0 -253
package/README.md
CHANGED
|
@@ -1,76 +1,31 @@
|
|
|
1
|
-
#
|
|
1
|
+
# `@envarai/envarpay`
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
1
|
+
type Name = 'ledger_read' | 'payment_unknown' | 'execution_unknown' | 'report_backlog' | 'report_failure';
|
|
2
2
|
type Signal = {
|
|
3
|
-
name:
|
|
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" | "
|
|
13
|
+
role: "unavailable" | "seller";
|
|
24
14
|
view: "alerts";
|
|
25
15
|
observation: "local_persisted_state_only";
|
|
26
|
-
status:
|
|
16
|
+
status: "unavailable" | "partial" | "available";
|
|
27
17
|
signals: Signal[];
|
|
28
|
-
budgets:
|
|
18
|
+
budgets: never[];
|
|
29
19
|
alertCount: number;
|
|
30
20
|
unavailableCount: number;
|
|
31
21
|
providerHealth: "unavailable_not_probed";
|
package/dist/commerce/alerts.js
CHANGED
|
@@ -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
|
|
3
|
-
|
|
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())
|
|
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 = []
|
|
33
|
-
let
|
|
23
|
+
const signals = [];
|
|
24
|
+
let db;
|
|
25
|
+
let available = false;
|
|
34
26
|
try {
|
|
35
27
|
db = database(path);
|
|
36
|
-
|
|
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
|
-
|
|
43
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
121
|
-
|
|
122
|
-
|
|
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,
|
package/dist/commerce/card.js
CHANGED
|
@@ -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
|
|
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(', ')}`,
|
package/dist/commerce/cli.js
CHANGED
|
@@ -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,
|
|
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
|
|
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
|
|
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, {
|
|
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']
|
|
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
|
|
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 (
|
|
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
|
-
|
|
294
|
-
|
|
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
|
-
|
|
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 (!
|
|
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,
|
|
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
|
-
|
|
343
|
-
|
|
344
|
-
|
|
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
|
|
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',
|
|
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
|
|
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;
|