@envarai/envarpay 0.2.0-alpha.9 → 2026.1025.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. package/README.md +18 -56
  2. package/dist/commerce/agent-setup.d.ts +19 -0
  3. package/dist/commerce/agent-setup.js +210 -0
  4. package/dist/commerce/alerts.d.ts +5 -15
  5. package/dist/commerce/alerts.js +21 -77
  6. package/dist/commerce/card.js +1 -1
  7. package/dist/commerce/cli.js +74 -137
  8. package/dist/commerce/config.d.ts +1 -4
  9. package/dist/commerce/config.js +5 -81
  10. package/dist/commerce/configure.d.ts +2 -0
  11. package/dist/commerce/configure.js +78 -0
  12. package/dist/commerce/discovery.d.ts +20 -0
  13. package/dist/commerce/discovery.js +162 -0
  14. package/dist/commerce/envar-balance.d.ts +25 -0
  15. package/dist/commerce/envar-balance.js +65 -0
  16. package/dist/commerce/envar-runtime.d.ts +1 -0
  17. package/dist/commerce/envar-runtime.js +7 -0
  18. package/dist/commerce/envar.d.ts +13 -31
  19. package/dist/commerce/envar.js +35 -83
  20. package/dist/commerce/external-agent.d.ts +26 -0
  21. package/dist/commerce/external-agent.js +129 -0
  22. package/dist/commerce/external-events.d.ts +76 -0
  23. package/dist/commerce/external-events.js +273 -0
  24. package/dist/commerce/external-mcp.d.ts +10 -0
  25. package/dist/commerce/external-mcp.js +199 -0
  26. package/dist/commerce/external-oauth.d.ts +12 -0
  27. package/dist/commerce/external-oauth.js +230 -0
  28. package/dist/commerce/external-runtime.d.ts +25 -0
  29. package/dist/commerce/external-runtime.js +244 -0
  30. package/dist/commerce/managed.d.ts +3 -0
  31. package/dist/commerce/managed.js +143 -0
  32. package/dist/commerce/native-inventory.d.ts +9 -0
  33. package/dist/commerce/native-inventory.js +75 -0
  34. package/dist/commerce/native-skills.d.ts +8 -0
  35. package/dist/commerce/native-skills.js +11 -8
  36. package/dist/commerce/openclaw-setup.d.ts +28 -0
  37. package/dist/commerce/openclaw-setup.js +43 -0
  38. package/dist/commerce/operations.d.ts +1 -2
  39. package/dist/commerce/operations.js +17 -20
  40. package/dist/commerce/pricing.d.ts +0 -1
  41. package/dist/commerce/pricing.js +1 -3
  42. package/dist/commerce/runtime.d.ts +2 -2
  43. package/dist/commerce/runtime.js +2 -2
  44. package/dist/commerce/server.d.ts +0 -1
  45. package/dist/commerce/server.js +2 -3
  46. package/dist/commerce/setup.d.ts +25 -0
  47. package/dist/commerce/setup.js +379 -0
  48. package/dist/commerce/store.d.ts +11 -1
  49. package/dist/commerce/store.js +36 -2
  50. package/dist/commerce/types.d.ts +1 -56
  51. package/examples/envar-integration-policy.json +1 -9
  52. package/examples/seller.json +143 -26
  53. package/package.json +11 -20
  54. package/runtime/hermes-setup-probe.py +109 -0
  55. package/runtime/hermes-skill-inventory.py +108 -0
  56. package/schemas/envarpay.config.schema.json +7 -73
  57. package/dist/commerce/buyer-store.d.ts +0 -101
  58. package/dist/commerce/buyer-store.js +0 -168
  59. package/dist/commerce/buyer.d.ts +0 -138
  60. package/dist/commerce/buyer.js +0 -775
  61. package/dist/commerce/chain.d.ts +0 -24
  62. package/dist/commerce/chain.js +0 -146
  63. package/dist/commerce/client-runtime.d.ts +0 -8
  64. package/dist/commerce/client-runtime.js +0 -7
  65. package/dist/commerce/management.d.ts +0 -14
  66. package/dist/commerce/management.js +0 -140
  67. package/dist/commerce/mpp-client.d.ts +0 -70
  68. package/dist/commerce/mpp-client.js +0 -29
  69. package/dist/commerce/mpp-runtime.d.ts +0 -4
  70. package/dist/commerce/mpp-runtime.js +0 -4
  71. package/dist/commerce/mpp.d.ts +0 -53
  72. package/dist/commerce/mpp.js +0 -163
  73. package/dist/commerce/peer-proxy.d.ts +0 -44
  74. package/dist/commerce/peer-proxy.js +0 -673
  75. package/dist/commerce/stripe-issuer.d.ts +0 -41
  76. package/dist/commerce/stripe-issuer.js +0 -351
  77. package/dist/commerce/stripe-provider.d.ts +0 -11
  78. package/dist/commerce/stripe-provider.js +0 -132
  79. package/dist/commerce/x402.d.ts +0 -38
  80. package/dist/commerce/x402.js +0 -181
  81. package/dist/index.d.ts +0 -95
  82. package/dist/index.js +0 -163
  83. package/examples/buyer-policy.json +0 -18
  84. package/schemas/buyer-policy.schema.json +0 -253
  85. /package/{examples → runtime}/native-skill-runner.py +0 -0
package/README.md CHANGED
@@ -1,69 +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
- Node.js 22.14+ (Node 24 recommended). SQLite is single-instance. Contracts can be
15
- read without loading the seller or buyer runtime; Bun validation covers the
16
- contracts entry, not the SQLite server.
17
-
18
- | Entry | Purpose |
19
- |---|---|
20
- | `@envarai/envarpay` | Config types/validation, integer pricing, quotes and A2A Cards |
21
- | `/server` | Durable seller A2A handler, exact x402 gate and native upstream executor |
22
- | `/client` | Buyer policy, atomic budgets, original-operation recovery and private management |
23
- | `/mpp` | Opt-in native MPP seller and read-only Stripe reconciliation |
24
- | `/envar` | Optional bounded config pull/ack and durable observation reports |
7
+ Use the Agent-specific setup file downloaded from Envar:
25
8
 
26
9
  ```sh
27
- npm ci --ignore-scripts
28
- npm run build
29
- node dist/commerce/cli.js init --directory ./private
30
- node dist/commerce/cli.js validate --config ./private/seller.json
10
+ envarpay setup --file /private/path/envar-setup.json
31
11
  ```
32
12
 
33
- Initialization writes examples and a vault key; it never creates a purchase or
34
- funds a wallet. Replace example addresses/hosts and review service limits before
35
- starting. Keep Agent model credentials in the Agent, and signer keys in a separate
36
- 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.
37
14
 
38
- ```sh
39
- node dist/commerce/cli.js serve --config seller.json \
40
- --credentials private/seller-auth.json --state private/seller.sqlite3 \
41
- --origin https://seller.example --port 4020
42
- node dist/commerce/cli.js buyer-serve --config buyer-policy.json \
43
- --credentials private/buyer-auth.json --state private/buyer.sqlite3 \
44
- --origin https://buyer-management.example --port 4021
45
- ```
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.
46
16
 
47
- The management service is private and owner-authenticated. A website may request a
48
- quote and confirm an already-reviewed purchase; it cannot change peers, recipient
49
- allowlists, budgets or signing keys. Unknown outcomes retain the original request,
50
- signature/SPT and reserved budget. Never create a replacement purchase after a
51
- timeout.
17
+ ## Manual seller commands
52
18
 
53
- [Seller and wire behavior](../../docs/a2a-commerce.md) ·
54
- [Buyer management](../../docs/a2a-buyer.md) ·
55
- [Continuation and recovery](../../docs/a2a-runtime-recovery.md) ·
56
- [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
+ ```
57
26
 
58
- ## 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.
59
28
 
60
- Payment confirmation, task completion and acceptance are separate facts. The MVP
61
- charges upfront for a new task; reads and permitted clarification use that same
62
- purchase. It does not provide escrow, automatic refunds, subscriptions or billing
63
- for arbitrary internal tools. A2A compatibility alone does not give an Agent a
64
- 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.
65
30
 
66
- MPP requires an eligible Stripe merchant, a permitted buyer token-creation flow,
67
- explicit test/live mode and any required customer authentication. Simulator tests
68
- are not card charges. Provider and chain acceptance must be recorded separately
69
- 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.
@@ -0,0 +1,19 @@
1
+ interface Invitation {
2
+ version: 1;
3
+ agentId: string;
4
+ platformOrigin: string;
5
+ setupToken: string;
6
+ }
7
+ export declare function invitation(value: any): Invitation;
8
+ export declare function agentSetup(options: {
9
+ file: string;
10
+ directory?: string;
11
+ python?: string;
12
+ home?: string;
13
+ container?: string;
14
+ origin?: string;
15
+ port?: number;
16
+ checkOnly?: boolean;
17
+ framework?: "hermes" | "openclaw";
18
+ }): Promise<void>;
19
+ export {};
@@ -0,0 +1,210 @@
1
+ /** Agent-driven installation. Secrets stay in local files; receipts come from Envar. */
2
+ import { existsSync, lstatSync, mkdirSync, readFileSync, writeFileSync, openSync, closeSync } from 'node:fs';
3
+ import { homedir } from 'node:os';
4
+ import { join, resolve } from 'node:path';
5
+ import { createServer } from 'node:net';
6
+ import { spawn } from 'node:child_process';
7
+ import { activateManagedProfile, managedProfile } from './managed.js';
8
+ import { setupAgent, parseSetupBundle } from './setup.js';
9
+ import { CommerceError } from './types.js';
10
+ function privateJson(file) {
11
+ const s = lstatSync(file);
12
+ if (!s.isFile() || s.isSymbolicLink() || s.size > 16384 || (s.mode & 0o077))
13
+ throw new CommerceError('setup_private_file', 'Save the setup invitation in an owner-only file (chmod600).');
14
+ return JSON.parse(readFileSync(file, 'utf8'));
15
+ }
16
+ export function invitation(value) {
17
+ if (value?.version !== 1 || !/^[0-9a-f-]{36}$/.test(value.agentId) || !/^envar_setup_[A-Za-z0-9_-]{43}$/.test(value.setupToken))
18
+ throw new CommerceError('setup_invitation', 'Copy a fresh setup prompt.');
19
+ const url = new URL(value.platformOrigin);
20
+ if (url.protocol !== 'https:' || url.origin !== value.platformOrigin || url.username || url.password)
21
+ throw new CommerceError('setup_origin', 'Use the exact Envar HTTPS origin.');
22
+ return value;
23
+ }
24
+ async function request(invite, path, data) {
25
+ const r = await fetch(invite.platformOrigin + '/api/v1/agent-setup/' + path, { method: 'POST', redirect: 'error', headers: { Authorization: 'Bearer ' + invite.setupToken, 'Content-Type': 'application/json' }, body: JSON.stringify(data), signal: AbortSignal.timeout(45000) });
26
+ if (!r.ok)
27
+ throw new CommerceError('setup_' + path, `Envar ${path} returned ${r.status}. Check connection or copy a fresh prompt; do not report success.`);
28
+ const text = await r.text();
29
+ if (text.length > 65536)
30
+ throw new CommerceError('setup_response', 'Envar returned an oversized response.');
31
+ return JSON.parse(text);
32
+ }
33
+ async function freePort() {
34
+ const socket = createServer();
35
+ await new Promise((resolve, reject) => { socket.once('error', reject); socket.listen(0, '127.0.0.1', resolve); });
36
+ const port = socket.address().port;
37
+ await new Promise(resolve => socket.close(() => resolve()));
38
+ return port;
39
+ }
40
+ function save(file, data) { writeFileSync(file, JSON.stringify(data, null, 2) + '\n', { mode: 0o600, flag: 'wx' }); }
41
+ async function publicOrigin(root, port, agentId, supplied) {
42
+ if (supplied) {
43
+ const u = new URL(supplied);
44
+ if (u.protocol !== 'https:' || u.origin !== supplied)
45
+ throw Error('Use a public HTTPS origin');
46
+ return supplied;
47
+ }
48
+ // Use an available existing tunnel session, without deleting or rebinding any endpoint.
49
+ for (const apiPort of [4040, 4041, 4042, 4043]) {
50
+ try {
51
+ const api = `http://127.0.0.1:${apiPort}`;
52
+ const r = await fetch(api + '/api/tunnels', { signal: AbortSignal.timeout(1000) });
53
+ if (!r.ok)
54
+ continue;
55
+ const list = (await r.json());
56
+ const name = 'envarpay-' + agentId, addr = `http://127.0.0.1:${port}`;
57
+ const prior = list.tunnels?.find((t) => t.name === name);
58
+ if (prior) {
59
+ if (prior.config.addr !== addr)
60
+ continue;
61
+ return prior.public_url;
62
+ }
63
+ if (list.tunnels?.length >= 3)
64
+ continue;
65
+ const created = await fetch(api + '/api/tunnels', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name, proto: 'http', addr, schemes: ['https'], inspect: false }), signal: AbortSignal.timeout(12000) });
66
+ if (created.ok)
67
+ return (await created.json()).public_url;
68
+ }
69
+ catch { }
70
+ }
71
+ // Outbound HTTPS works inside Docker too; never requires a Docker socket or host port mapping.
72
+ const log = join(root, 'tunnel.log');
73
+ if (existsSync(log))
74
+ privateLog(log);
75
+ const offset = existsSync(log) ? readFileSync(log, 'utf8').length : 0, fd = openSync(log, 'a', 0o600);
76
+ const tunnel = spawn('cloudflared', ['tunnel', '--url', `http://127.0.0.1:${port}`, '--no-autoupdate'], { detached: true, stdio: ['ignore', fd, fd] });
77
+ closeSync(fd);
78
+ let failed = false;
79
+ tunnel.on('error', () => { failed = true; });
80
+ tunnel.unref();
81
+ for (let i = 0; i < 45; i++) {
82
+ if (failed)
83
+ break;
84
+ await new Promise(resolve => setTimeout(resolve, 1000));
85
+ const url = readFileSync(log, 'utf8').slice(offset).match(/https:\/\/[a-z0-9-]+\.trycloudflare\.com\b/)?.[0];
86
+ if (url) {
87
+ writeFileSync(join(root, 'tunnel-process.json'), JSON.stringify({ pid: tunnel.pid, origin: url, temporary: true }), { mode: 0o600 });
88
+ return url;
89
+ }
90
+ }
91
+ if (tunnel.pid)
92
+ try {
93
+ tunnel.kill();
94
+ }
95
+ catch { }
96
+ throw new CommerceError('setup_https', 'Install cloudflared from its official distribution, or supply --origin with an existing HTTPS forwarder. Preserve other tunnels.');
97
+ }
98
+ function privateLog(file) { const s = lstatSync(file); if (!s.isFile() || s.isSymbolicLink() || (s.mode & 0o077))
99
+ throw new CommerceError('setup_private_file', 'Runtime log must be an owner-only regular file.'); }
100
+ export async function agentSetup(options) {
101
+ const invite = invitation(privateJson(resolve(options.file)));
102
+ const managedRoot = process.env.ENVARPAY_MANAGED_ROOT ? resolve(process.env.ENVARPAY_MANAGED_ROOT) : undefined;
103
+ const managedDirectory = managedRoot ? join(managedRoot, 'profiles', invite.agentId) : undefined;
104
+ const profile = resolve(options.directory ?? managedDirectory ?? join(homedir(), '.envarpay', invite.agentId));
105
+ if (managedRoot) {
106
+ if (options.container || profile !== managedDirectory)
107
+ throw new CommerceError('managed_profile', 'Use this managed runtime and its existing profile location');
108
+ const active = managedProfile(managedRoot);
109
+ if (active !== managedRoot && active !== profile)
110
+ throw new CommerceError('managed_bound', 'This runtime belongs to another Agent; preserve its original profile');
111
+ const origin = process.env.ENVARPAY_ORIGIN, port = Number(process.env.ENVARPAY_PORT ?? '4020');
112
+ if (!origin || new URL(origin).protocol !== 'https:' || new URL(origin).origin !== origin || !Number.isInteger(port) || port < 1024 || port > 65535 || (options.origin && options.origin !== origin) || (options.port && options.port !== port))
113
+ throw new CommerceError('managed_origin', 'Use the managed HTTPS origin and port');
114
+ options = { ...options, origin, port };
115
+ }
116
+ const root = profile + '.onboarding';
117
+ if (options.checkOnly) {
118
+ const result = await request(invite, 'status', {});
119
+ if (result.agent_id !== invite.agentId)
120
+ throw new CommerceError('setup_identity', 'Receipt belongs to another Agent.');
121
+ console.log(JSON.stringify(result, null, 2));
122
+ if (result.status !== 'connected')
123
+ process.exitCode = 1;
124
+ return;
125
+ }
126
+ mkdirSync(root, { recursive: true, mode: 0o700 });
127
+ if (lstatSync(root).isSymbolicLink() || (lstatSync(root).mode & 0o077))
128
+ throw new CommerceError('setup_directory', 'Use a private directory owned by this user.');
129
+ const manifest = join(root, 'setup.json');
130
+ if (existsSync(manifest) && privateJson(manifest).agentId !== invite.agentId)
131
+ throw new CommerceError('setup_identity', 'This directory belongs to another Agent.');
132
+ const bundle = parseSetupBundle(await request(invite, 'claim', {}));
133
+ if (bundle.agentId !== invite.agentId || bundle.platformOrigin !== invite.platformOrigin)
134
+ throw new CommerceError('setup_identity', 'Envar returned another Agent identity.');
135
+ const bundleFile = join(root, 'bundle.json');
136
+ if (existsSync(bundleFile))
137
+ privateJson(bundleFile);
138
+ writeFileSync(bundleFile, JSON.stringify(bundle), { mode: 0o600 });
139
+ if (!existsSync(profile)) {
140
+ let settings = existsSync(manifest) ? privateJson(manifest) : undefined;
141
+ if (!settings) {
142
+ const port = options.port ?? await freePort();
143
+ const origin = await publicOrigin(root, port, invite.agentId, options.origin);
144
+ settings = { agentId: invite.agentId, port, origin };
145
+ save(manifest, settings);
146
+ }
147
+ await setupAgent({ framework: options.framework, managed: !!managedRoot, file: bundleFile, directory: profile, python: options.python, home: options.home, container: options.container, automatic: settings });
148
+ }
149
+ if (lstatSync(profile).isSymbolicLink() || (lstatSync(profile).mode & 0o077))
150
+ throw new CommerceError('setup_directory', 'The runtime must be a private directory.');
151
+ const connection = privateJson(join(profile, 'connection.json'));
152
+ privateLog(join(profile, 'envar.token'));
153
+ const launcher = lstatSync(join(profile, 'start.mjs'));
154
+ if (!launcher.isFile() || launcher.isSymbolicLink() || (launcher.mode & 0o077))
155
+ throw new CommerceError('setup_private_file', 'Use the original private runtime launcher.');
156
+ if (connection.agentId !== invite.agentId)
157
+ throw new CommerceError('setup_identity', 'Existing runtime belongs to another Agent.');
158
+ const policy = privateJson(join(profile, 'envar.json')).policy;
159
+ if (policy.agentId !== invite.agentId || policy.platformOrigin !== invite.platformOrigin)
160
+ throw new CommerceError('setup_identity', 'Existing integration belongs to another Agent.');
161
+ if (managedRoot) {
162
+ if (readFileSync(join(profile, 'envar.token'), 'utf8') !== bundle.token)
163
+ writeFileSync(join(profile, 'envar.token'), bundle.token, { mode: 0o600 });
164
+ activateManagedProfile(managedRoot, invite.agentId, profile);
165
+ }
166
+ else {
167
+ const running = join(root, 'process.json');
168
+ let started = false;
169
+ if (existsSync(running))
170
+ try {
171
+ process.kill(privateJson(running).pid, 0);
172
+ started = true;
173
+ }
174
+ catch { }
175
+ if (started && readFileSync(join(profile, 'envar.token'), 'utf8') !== bundle.token)
176
+ throw new CommerceError('setup_running', 'Existing runtime uses an earlier credential. Stop its recorded process, keep its ledger, then retry this invitation.');
177
+ if (!started) {
178
+ privateLog(join(profile, 'envar.token'));
179
+ writeFileSync(join(profile, 'envar.token'), bundle.token, { mode: 0o600 });
180
+ const log = join(root, 'runtime.log');
181
+ if (existsSync(log))
182
+ privateLog(log);
183
+ const fd = openSync(log, 'a', 0o600), child = spawn(process.execPath, [join(profile, 'start.mjs')], { detached: true, stdio: ['ignore', fd, fd] });
184
+ closeSync(fd);
185
+ child.unref();
186
+ writeFileSync(running, JSON.stringify({ pid: child.pid }), { mode: 0o600 });
187
+ }
188
+ }
189
+ let result;
190
+ for (let attempt = 0; attempt < 12; attempt++) {
191
+ try {
192
+ result = await request(invite, 'connect', { agentId: invite.agentId, url: connection.url, authToken: connection.authToken });
193
+ if (result.status === 'connected')
194
+ break;
195
+ }
196
+ catch (e) {
197
+ if (attempt === 11)
198
+ throw e;
199
+ }
200
+ await new Promise(resolve => setTimeout(resolve, 3000));
201
+ }
202
+ result = await request(invite, 'status', {});
203
+ if (result.status !== 'connected' || result.agent_id !== invite.agentId || !result.verified_at)
204
+ throw new CommerceError('setup_unverified', 'Platform did not verify this setup. Keep files and report the blocker.');
205
+ const output = { ...result, local_directory: profile, receipt_file: join(root, 'receipt.json'), scope: 'connected; not published; no payment', restart: managedRoot ? 'managed by the deployment' : `node ${JSON.stringify(join(profile, 'start.mjs'))}`, temporary_tunnel: !options.origin, automatic_restart: !!managedRoot };
206
+ if (existsSync(join(root, 'receipt.json')))
207
+ privateJson(join(root, 'receipt.json'));
208
+ writeFileSync(join(root, 'receipt.json'), JSON.stringify(output, null, 2) + '\n', { mode: 0o600 });
209
+ console.log(JSON.stringify(output, null, 2));
210
+ }
@@ -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: '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(', ')}`,