@envarai/envarpay 0.2.0-alpha.2 → 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 (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 +27 -0
  5. package/dist/commerce/alerts.js +71 -0
  6. package/dist/commerce/card.js +3 -3
  7. package/dist/commerce/cli.js +91 -119
  8. package/dist/commerce/config.d.ts +3 -2
  9. package/dist/commerce/config.js +19 -77
  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 +3 -1
  17. package/dist/commerce/envar-runtime.js +28 -1
  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/index.d.ts +1 -0
  31. package/dist/commerce/index.js +1 -0
  32. package/dist/commerce/managed.d.ts +3 -0
  33. package/dist/commerce/managed.js +143 -0
  34. package/dist/commerce/native-inventory.d.ts +9 -0
  35. package/dist/commerce/native-inventory.js +75 -0
  36. package/dist/commerce/native-skills.d.ts +32 -0
  37. package/dist/commerce/native-skills.js +174 -0
  38. package/dist/commerce/openclaw-setup.d.ts +28 -0
  39. package/dist/commerce/openclaw-setup.js +43 -0
  40. package/dist/commerce/operations.d.ts +8 -3
  41. package/dist/commerce/operations.js +21 -18
  42. package/dist/commerce/pricing.d.ts +0 -1
  43. package/dist/commerce/pricing.js +3 -5
  44. package/dist/commerce/runtime.d.ts +4 -2
  45. package/dist/commerce/runtime.js +4 -2
  46. package/dist/commerce/server.d.ts +3 -2
  47. package/dist/commerce/server.js +26 -10
  48. package/dist/commerce/setup.d.ts +25 -0
  49. package/dist/commerce/setup.js +379 -0
  50. package/dist/commerce/skill-gate.d.ts +54 -0
  51. package/dist/commerce/skill-gate.js +153 -0
  52. package/dist/commerce/store.d.ts +11 -1
  53. package/dist/commerce/store.js +36 -2
  54. package/dist/commerce/types.d.ts +6 -57
  55. package/examples/envar-integration-policy.json +1 -9
  56. package/examples/hermes-setup-probe.py +109 -0
  57. package/examples/hermes-skill-inventory.py +108 -0
  58. package/examples/native-skill-runner.py +197 -0
  59. package/examples/seller.json +143 -26
  60. package/package.json +5 -16
  61. package/schemas/envarpay.config.schema.json +28 -87
  62. package/dist/commerce/buyer-store.d.ts +0 -101
  63. package/dist/commerce/buyer-store.js +0 -168
  64. package/dist/commerce/buyer.d.ts +0 -132
  65. package/dist/commerce/buyer.js +0 -755
  66. package/dist/commerce/chain.d.ts +0 -24
  67. package/dist/commerce/chain.js +0 -146
  68. package/dist/commerce/client-runtime.d.ts +0 -8
  69. package/dist/commerce/client-runtime.js +0 -7
  70. package/dist/commerce/management.d.ts +0 -14
  71. package/dist/commerce/management.js +0 -138
  72. package/dist/commerce/mpp-client.d.ts +0 -59
  73. package/dist/commerce/mpp-client.js +0 -24
  74. package/dist/commerce/mpp-runtime.d.ts +0 -3
  75. package/dist/commerce/mpp-runtime.js +0 -3
  76. package/dist/commerce/mpp.d.ts +0 -53
  77. package/dist/commerce/mpp.js +0 -163
  78. package/dist/commerce/peer-proxy.d.ts +0 -44
  79. package/dist/commerce/peer-proxy.js +0 -673
  80. package/dist/commerce/stripe-provider.d.ts +0 -9
  81. package/dist/commerce/stripe-provider.js +0 -97
  82. package/dist/commerce/x402.d.ts +0 -37
  83. package/dist/commerce/x402.js +0 -174
  84. package/examples/buyer-policy.json +0 -18
  85. package/schemas/buyer-policy.schema.json +0 -253
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
+ }
@@ -0,0 +1,27 @@
1
+ type Name = 'ledger_read' | 'payment_unknown' | 'execution_unknown' | 'report_backlog' | 'report_failure';
2
+ type Signal = {
3
+ name: Name;
4
+ status: 'clear' | 'alert' | 'unavailable' | 'not_applicable';
5
+ severity: 'info' | 'warning' | 'critical';
6
+ count: number | null;
7
+ };
8
+ export interface LocalAlertOptions {
9
+ integrationPath?: string;
10
+ integrationDisabled?: boolean;
11
+ }
12
+ export declare function inspectLocalAlerts(path: string, options?: LocalAlertOptions): {
13
+ role: "unavailable" | "seller";
14
+ view: "alerts";
15
+ observation: "local_persisted_state_only";
16
+ status: "unavailable" | "partial" | "available";
17
+ signals: Signal[];
18
+ budgets: never[];
19
+ alertCount: number;
20
+ unavailableCount: number;
21
+ providerHealth: "unavailable_not_probed";
22
+ recommendedExitCode: number;
23
+ paymentPerformed: boolean;
24
+ refundPerformed: boolean;
25
+ networkRequests: number;
26
+ };
27
+ export {};
@@ -0,0 +1,71 @@
1
+ /** Read-only health signals for one seller ledger and its Envar report queue. */
2
+ import { DatabaseSync } from 'node:sqlite';
3
+ import { lstatSync } from 'node:fs';
4
+ function database(path) {
5
+ const stat = lstatSync(path);
6
+ if (!stat.isFile() || stat.isSymbolicLink() || (stat.mode & 0o077) !== 0 || (process.getuid && stat.uid !== process.getuid()))
7
+ throw new Error('private_source_unavailable');
8
+ const db = new DatabaseSync(path, { readOnly: true });
9
+ try {
10
+ db.exec('PRAGMA query_only=ON; PRAGMA busy_timeout=1000; BEGIN');
11
+ return db;
12
+ }
13
+ catch (error) {
14
+ db.close();
15
+ throw error;
16
+ }
17
+ }
18
+ const count = (db, sql) => Number(db.prepare(sql).get().n);
19
+ const signal = (name, value, severity = 'warning') => ({ name, status: value ? 'alert' : 'clear', severity: value ? severity : 'info', count: value });
20
+ const missing = (name) => ({ name, status: 'unavailable', severity: 'warning', count: null });
21
+ const irrelevant = (name) => ({ name, status: 'not_applicable', severity: 'info', count: null });
22
+ export function inspectLocalAlerts(path, options = {}) {
23
+ const signals = [];
24
+ let db;
25
+ let available = false;
26
+ try {
27
+ db = database(path);
28
+ if (db.prepare("SELECT version FROM commerce_meta").get()?.version !== 2)
29
+ throw new Error('unknown_version');
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;
32
+ }
33
+ catch {
34
+ signals.push(missing('ledger_read'), missing('payment_unknown'), missing('execution_unknown'));
35
+ }
36
+ finally {
37
+ db?.close();
38
+ }
39
+ let integration;
40
+ try {
41
+ if (options.integrationDisabled && !options.integrationPath) {
42
+ signals.push(irrelevant('report_backlog'), irrelevant('report_failure'));
43
+ }
44
+ else {
45
+ if (options.integrationDisabled || !options.integrationPath)
46
+ throw new Error('integration_not_supplied');
47
+ integration = database(options.integrationPath);
48
+ if (!integration.prepare("SELECT value FROM meta WHERE key='binding'").get() || !integration.prepare("SELECT value FROM meta WHERE key='source_instance'").get())
49
+ throw new Error('integration_schema');
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<>''")));
51
+ }
52
+ }
53
+ catch {
54
+ signals.push(missing('report_backlog'), missing('report_failure'));
55
+ }
56
+ finally {
57
+ integration?.close();
58
+ }
59
+ const alertCount = signals.filter(s => s.status === 'alert').length;
60
+ const unavailableCount = signals.filter(s => s.status === 'unavailable').length;
61
+ return {
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,
67
+ providerHealth: 'unavailable_not_probed',
68
+ recommendedExitCode: unavailableCount ? 3 : alertCount ? 2 : 0,
69
+ paymentPerformed: false, refundPerformed: false, networkRequests: 0,
70
+ };
71
+ }
@@ -1,6 +1,6 @@
1
1
  import { AgentCard } from '@a2a-js/sdk';
2
2
  import { CommerceError } from './types.js';
3
- import { validateUrl, digest } from './config.js';
3
+ import { validateUrl, serviceTermsDigest } from './config.js';
4
4
  /** One deterministic offer route, serialized by the official A2A 1.0 codec. */
5
5
  export function offerPath(serviceId, revision, offerId) {
6
6
  return `/services/${encodeURIComponent(serviceId)}/v${revision}/offers/${encodeURIComponent(offerId)}`;
@@ -14,13 +14,13 @@ 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(', ')}`,
21
21
  version: String(service.revision),
22
22
  supportedInterfaces: [{ url: new URL(`${offerPath(service.id, service.revision, offer.id)}/a2a`, base).href, protocolBinding: 'JSONRPC', protocolVersion: '1.0' }],
23
- capabilities: { streaming: false, pushNotifications: false, extensions: [{ uri: 'urn:envarpay:commerce:1', required: false, description: 'Optional service offer metadata; standard A2A and payment transports remain independently usable', params: { serviceId: service.id, serviceRevision: service.revision, offerId: offer.id, pricing: offer.pricing, collection: offer.collection, contract: service.contract, payment, termsDigest: digest({ contract: service.contract, offer, profile }) } }] },
23
+ capabilities: { streaming: false, pushNotifications: false, extensions: [{ uri: 'urn:envarpay:commerce:1', required: false, description: 'Optional service offer metadata; standard A2A and payment transports remain independently usable', params: { serviceId: service.id, serviceRevision: service.revision, offerId: offer.id, pricing: offer.pricing, collection: offer.collection, contract: service.contract, payment, termsDigest: serviceTermsDigest(config.configVersion, service, offer, profile), ...(config.configVersion === 2 && service.execution.type === 'skill' ? { skillDigest: service.execution.skillDigest } : {}) } }] },
24
24
  defaultInputModes: ['application/json'], defaultOutputModes: ['application/json', 'text/plain'],
25
25
  skills: [{ id: service.id, name: service.name, description: `Purchase ${service.name}`, tags: [service.id] }],
26
26
  securitySchemes: { bearer: { httpAuthSecurityScheme: { scheme: 'bearer' } } },