@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.
- package/README.md +18 -56
- package/dist/commerce/agent-setup.d.ts +19 -0
- package/dist/commerce/agent-setup.js +210 -0
- package/dist/commerce/alerts.d.ts +27 -0
- package/dist/commerce/alerts.js +71 -0
- package/dist/commerce/card.js +3 -3
- package/dist/commerce/cli.js +91 -119
- package/dist/commerce/config.d.ts +3 -2
- package/dist/commerce/config.js +19 -77
- package/dist/commerce/configure.d.ts +2 -0
- package/dist/commerce/configure.js +78 -0
- package/dist/commerce/discovery.d.ts +20 -0
- package/dist/commerce/discovery.js +162 -0
- package/dist/commerce/envar-balance.d.ts +25 -0
- package/dist/commerce/envar-balance.js +65 -0
- package/dist/commerce/envar-runtime.d.ts +3 -1
- package/dist/commerce/envar-runtime.js +28 -1
- package/dist/commerce/envar.d.ts +13 -31
- package/dist/commerce/envar.js +35 -83
- package/dist/commerce/external-agent.d.ts +26 -0
- package/dist/commerce/external-agent.js +129 -0
- package/dist/commerce/external-events.d.ts +76 -0
- package/dist/commerce/external-events.js +273 -0
- package/dist/commerce/external-mcp.d.ts +10 -0
- package/dist/commerce/external-mcp.js +199 -0
- package/dist/commerce/external-oauth.d.ts +12 -0
- package/dist/commerce/external-oauth.js +230 -0
- package/dist/commerce/external-runtime.d.ts +25 -0
- package/dist/commerce/external-runtime.js +244 -0
- package/dist/commerce/index.d.ts +1 -0
- package/dist/commerce/index.js +1 -0
- package/dist/commerce/managed.d.ts +3 -0
- package/dist/commerce/managed.js +143 -0
- package/dist/commerce/native-inventory.d.ts +9 -0
- package/dist/commerce/native-inventory.js +75 -0
- package/dist/commerce/native-skills.d.ts +32 -0
- package/dist/commerce/native-skills.js +174 -0
- package/dist/commerce/openclaw-setup.d.ts +28 -0
- package/dist/commerce/openclaw-setup.js +43 -0
- package/dist/commerce/operations.d.ts +8 -3
- package/dist/commerce/operations.js +21 -18
- package/dist/commerce/pricing.d.ts +0 -1
- package/dist/commerce/pricing.js +3 -5
- package/dist/commerce/runtime.d.ts +4 -2
- package/dist/commerce/runtime.js +4 -2
- package/dist/commerce/server.d.ts +3 -2
- package/dist/commerce/server.js +26 -10
- package/dist/commerce/setup.d.ts +25 -0
- package/dist/commerce/setup.js +379 -0
- package/dist/commerce/skill-gate.d.ts +54 -0
- package/dist/commerce/skill-gate.js +153 -0
- package/dist/commerce/store.d.ts +11 -1
- package/dist/commerce/store.js +36 -2
- package/dist/commerce/types.d.ts +6 -57
- package/examples/envar-integration-policy.json +1 -9
- package/examples/hermes-setup-probe.py +109 -0
- package/examples/hermes-skill-inventory.py +108 -0
- package/examples/native-skill-runner.py +197 -0
- package/examples/seller.json +143 -26
- package/package.json +5 -16
- package/schemas/envarpay.config.schema.json +28 -87
- package/dist/commerce/buyer-store.d.ts +0 -101
- package/dist/commerce/buyer-store.js +0 -168
- package/dist/commerce/buyer.d.ts +0 -132
- package/dist/commerce/buyer.js +0 -755
- package/dist/commerce/chain.d.ts +0 -24
- package/dist/commerce/chain.js +0 -146
- 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 -138
- package/dist/commerce/mpp-client.d.ts +0 -59
- package/dist/commerce/mpp-client.js +0 -24
- package/dist/commerce/mpp-runtime.d.ts +0 -3
- package/dist/commerce/mpp-runtime.js +0 -3
- 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-provider.d.ts +0 -9
- package/dist/commerce/stripe-provider.js +0 -97
- package/dist/commerce/x402.d.ts +0 -37
- package/dist/commerce/x402.js +0 -174
- package/examples/buyer-policy.json +0 -18
- package/schemas/buyer-policy.schema.json +0 -253
package/dist/commerce/cli.js
CHANGED
|
@@ -1,14 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { readFileSync, lstatSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { readFileSync, lstatSync, mkdirSync, writeFileSync, readdirSync } from 'node:fs';
|
|
3
3
|
import { parseArgs } from 'node:util';
|
|
4
4
|
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,17 +14,24 @@ 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 {
|
|
20
|
-
import { evmReceiptVerifier, evmSettlementRecovery } from './chain.js';
|
|
17
|
+
import { EnvarBalanceGate } from './envar-balance.js';
|
|
21
18
|
import { CommerceError } from './types.js';
|
|
22
|
-
import {
|
|
19
|
+
import { createNativeSkillExecutor } from './native-skills.js';
|
|
20
|
+
import { inspectLedger, inspectLocalAlerts } from './operations.js';
|
|
21
|
+
import { serveManaged } from './managed.js';
|
|
22
|
+
import { setupAgent } from './setup.js';
|
|
23
|
+
import { SkillDiscovery } from './discovery.js';
|
|
24
|
+
import { upgradeDiscovery } from './configure.js';
|
|
25
|
+
import { agentSetup } from './agent-setup.js';
|
|
26
|
+
import { setupExternalAgent, serveExternalAgent } from './external-runtime.js';
|
|
23
27
|
const { values, positionals } = parseArgs({ allowPositionals: true, options: {
|
|
24
28
|
config: { type: 'string' }, service: { type: 'string' }, offer: { type: 'string' }, origin: { type: 'string' },
|
|
25
29
|
version: { type: 'boolean' }, help: { type: 'boolean', short: 'h' }, state: { type: 'string' }, credentials: { type: 'string' }, host: { type: 'string' },
|
|
26
30
|
port: { type: 'string' }, directory: { type: 'string' }, 'envar-config': { type: 'string' }, 'envar-credentials': { type: 'string' },
|
|
27
|
-
after: { type: 'string' }, limit: { type: 'string' },
|
|
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' },
|
|
32
|
+
framework: { type: 'string' }, file: { type: 'string' }, 'docker-container': { type: 'string' }, python: { type: 'string' }, 'hermes-home': { type: 'string' }, check: { type: 'boolean' },
|
|
28
33
|
} });
|
|
29
|
-
const usage = 'envarpay init --directory DIR | inspect --state DB [--after ROW --limit 100] | validate --config seller.json | card --config seller.json --service ID --offer ID --origin URL | serve
|
|
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';
|
|
30
35
|
const closers = [];
|
|
31
36
|
function privateFile(path) {
|
|
32
37
|
const stat = lstatSync(path);
|
|
@@ -66,16 +71,6 @@ function signals() {
|
|
|
66
71
|
async function closeHttp(http) {
|
|
67
72
|
await new Promise((resolve, reject) => http.close(error => error ? reject(error) : resolve()));
|
|
68
73
|
}
|
|
69
|
-
async function loadMppAdapter(path) {
|
|
70
|
-
privateFile(path);
|
|
71
|
-
const module = await import(pathToFileURL(resolve(path)).href);
|
|
72
|
-
if (typeof module.createMppBuyerOptions !== 'function')
|
|
73
|
-
throw new CommerceError('mpp_adapter_module', 'Owner-supplied module must export createMppBuyerOptions');
|
|
74
|
-
const options = await module.createMppBuyerOptions();
|
|
75
|
-
if (!options || typeof options.createToken !== 'function' || typeof options.recoverToken !== 'function' || typeof options.verifyReceipt !== 'function')
|
|
76
|
-
throw new CommerceError('mpp_adapter_module', 'MPP requires real token creation, original-operation recovery and receipt verification');
|
|
77
|
-
return options;
|
|
78
|
-
}
|
|
79
74
|
async function run() {
|
|
80
75
|
if (values.help || positionals.length === 0 && !values.version) {
|
|
81
76
|
console.log(usage);
|
|
@@ -86,17 +81,68 @@ async function run() {
|
|
|
86
81
|
return;
|
|
87
82
|
}
|
|
88
83
|
const command = positionals[0];
|
|
84
|
+
if (command === 'external-setup') {
|
|
85
|
+
if (!values.directory || !values.origin)
|
|
86
|
+
throw new CommerceError('external_setup', 'Provide directory and public HTTPS origin');
|
|
87
|
+
console.log(JSON.stringify(setupExternalAgent(values.directory, values.origin)));
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
if (command === 'external-serve') {
|
|
91
|
+
if (!values.config || !values.credentials || !values.state || !values.origin)
|
|
92
|
+
throw new CommerceError('external_serve', 'Provide config, credentials, state and origin');
|
|
93
|
+
const runtime = serveExternalAgent({ configFile: values.config, credentialsFile: values.credentials, stateFile: values.state, origin: values.origin, host: values.host ?? '127.0.0.1', port: port(4020) });
|
|
94
|
+
closers.push(() => runtime.close());
|
|
95
|
+
await once(runtime.http, 'listening');
|
|
96
|
+
signals();
|
|
97
|
+
console.log(JSON.stringify({ listening: port(4020), role: 'external-agent', paymentsEnabled: false }));
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
if (values.framework && !['hermes', 'openclaw'].includes(values.framework))
|
|
101
|
+
throw new CommerceError('setup_runtime', 'Use hermes or openclaw');
|
|
102
|
+
if (command === 'managed-serve') {
|
|
103
|
+
if (!values.directory || !values.origin)
|
|
104
|
+
throw new CommerceError('managed_configuration', 'Managed serve requires its directory and HTTPS origin');
|
|
105
|
+
await serveManaged(values.directory, values.origin, values.host ?? '127.0.0.1', port(4020));
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
if (command === 'onboard') {
|
|
109
|
+
if (!values.file)
|
|
110
|
+
throw new CommerceError('setup_file', 'Save the private invitation and pass --file PATH.');
|
|
111
|
+
await agentSetup({ framework: values.framework, file: values.file, directory: values.directory, python: values.python, home: values['hermes-home'], container: values['docker-container'], origin: values.origin, port: values.port ? port(4020) : undefined, checkOnly: values.check });
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
if (command === 'upgrade' && values.directory) {
|
|
115
|
+
upgradeDiscovery(values.directory, values['hermes-home']);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
if (command === 'setup') {
|
|
119
|
+
if (!values.file)
|
|
120
|
+
throw new CommerceError('setup_file', 'Download the setup file from your Agent service page and pass --file PATH');
|
|
121
|
+
await setupAgent({ framework: values.framework, file: values.file, directory: values.directory, container: values['docker-container'], python: values.python, home: values['hermes-home'] });
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
if (values.alerts || values['fail-on-alert'] || values['integration-state'] || values['no-integration']) {
|
|
125
|
+
if (command !== 'inspect' || !values.state || !values.alerts || values['refund-review'] || values.after || values.limit || (values['no-integration'] && values['integration-state']))
|
|
126
|
+
throw new CommerceError('inspection_view', 'Alert inspection needs inspect --state and --alerts, without pagination or refund review; choose at most one integration source');
|
|
127
|
+
const report = inspectLocalAlerts(values.state, { integrationPath: values['integration-state'], integrationDisabled: values['no-integration'] });
|
|
128
|
+
console.log(JSON.stringify(report, null, 2));
|
|
129
|
+
if (values['fail-on-alert'])
|
|
130
|
+
process.exitCode = report.recommendedExitCode;
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
89
133
|
if (command === 'inspect' && values.state) {
|
|
90
|
-
console.log(JSON.stringify(inspectLedger(values.state, Number(values.after ?? 0), Number(values.limit ?? 100)), null, 2));
|
|
134
|
+
console.log(JSON.stringify(inspectLedger(values.state, Number(values.after ?? 0), Number(values.limit ?? 100), values['refund-review'] ? 'refund-review' : 'attention'), null, 2));
|
|
91
135
|
return;
|
|
92
136
|
}
|
|
137
|
+
if (values['refund-review'])
|
|
138
|
+
throw new CommerceError('inspection_view', 'Refund review is available only for inspect');
|
|
93
139
|
if ((values['envar-config'] || values['envar-credentials']) && command !== 'serve')
|
|
94
140
|
throw new CommerceError('envar_role', 'Configuration synchronization is available on seller serve only');
|
|
95
141
|
if (Boolean(values['envar-config']) !== Boolean(values['envar-credentials']))
|
|
96
142
|
throw new CommerceError('envar_configuration', 'Supply both --envar-config and --envar-credentials');
|
|
97
143
|
if (command === 'init' && values.directory) {
|
|
98
144
|
mkdirSync(values.directory, { recursive: true, mode: 0o700 });
|
|
99
|
-
for (const [name, source] of [['seller.json', 'seller.json']
|
|
145
|
+
for (const [name, source] of [['seller.json', 'seller.json']]) {
|
|
100
146
|
writeFileSync(join(values.directory, name), readFileSync(new URL(`../../examples/${source}`, import.meta.url)), { flag: 'wx', mode: 0o600 });
|
|
101
147
|
}
|
|
102
148
|
writeFileSync(join(values.directory, 'vault.key'), randomBytes(32), { flag: 'wx', mode: 0o600, flush: true });
|
|
@@ -106,60 +152,6 @@ async function run() {
|
|
|
106
152
|
if (!values.config)
|
|
107
153
|
throw new CommerceError('usage', usage);
|
|
108
154
|
const raw = JSON.parse(readFileSync(values.config, 'utf8'));
|
|
109
|
-
if (command === 'buyer-serve') {
|
|
110
|
-
if (!values.origin || !values.state || !values.credentials)
|
|
111
|
-
throw new CommerceError('usage', usage);
|
|
112
|
-
const policy = loadBuyerPolicy(raw);
|
|
113
|
-
const secrets = JSON.parse(privateFile(values.credentials).toString('utf8'));
|
|
114
|
-
const needsEvm = policy.peers.some(peer => peer.protocol === 'x402'), needsMpp = policy.peers.some(peer => peer.protocol === 'mpp');
|
|
115
|
-
if (!secrets.peerTokens || (needsEvm || needsMpp) && !secrets.vaultKeyFile)
|
|
116
|
-
throw new CommerceError('buyer_credentials', 'Paid buyer needs private vault key and peer credentials');
|
|
117
|
-
let signer;
|
|
118
|
-
if (needsEvm) {
|
|
119
|
-
if (!secrets.privateKeyFile || !secrets.rpcUrls)
|
|
120
|
-
throw new CommerceError('buyer_credentials', 'x402 peers require local signer and RPC configuration');
|
|
121
|
-
const key = privateFile(secrets.privateKeyFile).toString('utf8').trim();
|
|
122
|
-
if (!/^0x[0-9a-fA-F]{64}$/.test(key))
|
|
123
|
-
throw new CommerceError('buyer_key', 'Signer file must contain one 32-byte 0x-prefixed private key');
|
|
124
|
-
signer = privateKeyToAccount(key);
|
|
125
|
-
}
|
|
126
|
-
if (needsMpp && !secrets.mppAdapterModule)
|
|
127
|
-
throw new CommerceError('mpp_adapter_module', 'MPP peers require an explicit local adapter module for authorized funding and receipt verification');
|
|
128
|
-
const mpp = needsMpp ? await loadMppAdapter(secrets.mppAdapterModule) : undefined;
|
|
129
|
-
const { BuyerStore } = await import('./buyer-store.js');
|
|
130
|
-
const { CommerceBuyer } = await import('./buyer.js');
|
|
131
|
-
const { BuyerManagement, listenBuyerManagement } = await import('./management.js');
|
|
132
|
-
const store = new BuyerStore(values.state);
|
|
133
|
-
closers.push(() => store.close());
|
|
134
|
-
const buyer = new CommerceBuyer({ policy, store, vault: secrets.vaultKeyFile ? new CredentialVault(join(dirname(values.state), 'buyer-authorizations'), privateFile(secrets.vaultKeyFile)) : undefined,
|
|
135
|
-
signer, mpp, peerTokens: secrets.peerTokens, peerEndpoints: secrets.peerEndpoints,
|
|
136
|
-
...(needsEvm ? { verifyReceipt: evmReceiptVerifier(secrets.rpcUrls), ...evmSettlementRecovery(secrets.rpcUrls) } : {}) });
|
|
137
|
-
closers.push(() => buyer.stop());
|
|
138
|
-
if (secrets.peerProxies?.length) {
|
|
139
|
-
if (secrets.peerProxies.length > 8)
|
|
140
|
-
throw new CommerceError('proxy_limit', 'Configure at most8 explicit peer proxies per buyer');
|
|
141
|
-
const { A2APeerProxy, listenPeerProxy } = await import('./peer-proxy.js');
|
|
142
|
-
for (const settings of secrets.peerProxies) {
|
|
143
|
-
if (Object.keys(settings.tokens).some(token => Object.hasOwn(secrets.callers, token)))
|
|
144
|
-
throw new CommerceError('proxy_token_scope', 'Agent proxy tokens must differ from owner management credentials');
|
|
145
|
-
if (!Number.isInteger(settings.port) || settings.port < 1 || settings.port > 65535)
|
|
146
|
-
throw new CommerceError('proxy_port', 'Invalid proxy listen port');
|
|
147
|
-
const proxy = new A2APeerProxy({ ...settings, buyer });
|
|
148
|
-
closers.push(async () => { await proxy.stop(); proxy.close(); });
|
|
149
|
-
const listener = listenPeerProxy(proxy, settings.host ?? '127.0.0.1', settings.port);
|
|
150
|
-
closers.push(() => closeHttp(listener));
|
|
151
|
-
await once(listener, 'listening');
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
const management = new BuyerManagement({ buyer, origin: values.origin, authenticate: bearerAuthenticator(secrets.callers) });
|
|
155
|
-
const http = listenBuyerManagement(management, values.origin, values.host ?? '127.0.0.1', port(4021));
|
|
156
|
-
closers.push(() => closeHttp(http));
|
|
157
|
-
await once(http, 'listening');
|
|
158
|
-
buyer.start();
|
|
159
|
-
signals();
|
|
160
|
-
console.log(JSON.stringify({ listening: port(4021), role: 'buyer', paymentsEnabled: policy.paymentsEnabled }));
|
|
161
|
-
return;
|
|
162
|
-
}
|
|
163
155
|
let config = loadCommerceConfig(raw);
|
|
164
156
|
if (command === 'validate') {
|
|
165
157
|
console.log(JSON.stringify({ valid: true, digest: digest(config), services: config.services.length, paymentPerformed: false }));
|
|
@@ -177,6 +169,10 @@ async function run() {
|
|
|
177
169
|
syncSettings = JSON.parse(privateFile(values['envar-config']).toString('utf8'));
|
|
178
170
|
if (!syncSettings?.policy)
|
|
179
171
|
throw new CommerceError('envar_configuration', 'Envar settings must include an explicit local policy');
|
|
172
|
+
if (secrets.skills?.discovery?.enabledFromWeb) {
|
|
173
|
+
const installed = readdirSync(secrets.skills.skillsDirectory, { withFileTypes: true }).filter(e => e.isDirectory() && !e.isSymbolicLink() && /^[a-z][a-z0-9-]{0,63}$/.test(e.name)).map(e => e.name);
|
|
174
|
+
syncSettings.policy.allowedServices = [...new Set([...syncSettings.policy.allowedServices, ...installed])];
|
|
175
|
+
}
|
|
180
176
|
const tokenFile = resolve(values['envar-credentials']);
|
|
181
177
|
privateFile(tokenFile);
|
|
182
178
|
integration = new EnvarIntegration({ policy: syncSettings.policy, stateDirectory: syncSettings.stateDirectory ?? join(dirname(values.state), 'envar-state'), configDirectory: syncSettings.configDirectory ?? join(dirname(values.state), 'envar-configs'), token: () => privateFile(tokenFile).toString('utf8').trim() });
|
|
@@ -187,68 +183,44 @@ async function run() {
|
|
|
187
183
|
closers.push(() => store.close());
|
|
188
184
|
if (integration)
|
|
189
185
|
config = restoreEnvarConfig(config, integration, store.catalogHistory());
|
|
190
|
-
const
|
|
191
|
-
for (const historical of store.catalogHistory())
|
|
192
|
-
for (const profile of Object.values(historical.profiles))
|
|
193
|
-
protocols.add(profile.adapter);
|
|
194
|
-
if (syncSettings?.policy.allowedX402.length)
|
|
195
|
-
protocols.add('x402');
|
|
196
|
-
if (syncSettings?.policy.allowedMppAccounts.length)
|
|
197
|
-
protocols.add('mpp');
|
|
186
|
+
const needsBalance = Boolean(integration) || Object.values(config.paymentProfiles).length > 0 || store.catalogHistory().some(entry => Object.values(entry.profiles).length > 0);
|
|
198
187
|
let vault;
|
|
199
|
-
if (
|
|
188
|
+
if (needsBalance) {
|
|
200
189
|
if (!secrets.vaultKeyFile)
|
|
201
190
|
throw new CommerceError('vault_key', 'Paid services require a private vault key file');
|
|
202
191
|
vault = new CredentialVault(join(dirname(values.state), 'authorizations'), privateFile(secrets.vaultKeyFile));
|
|
203
192
|
}
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
const gate = new X402Gate({ store, vault: vault, payerFor: caller => secrets.payers?.[caller], verifyReceipt: evmReceiptVerifier(secrets.rpcUrls), ...evmSettlementRecovery(secrets.rpcUrls) });
|
|
210
|
-
await gate.check(Object.values(config.paymentProfiles).filter((p) => p.adapter === 'x402'));
|
|
211
|
-
gates.x402 = gate;
|
|
212
|
-
}
|
|
213
|
-
if (protocols.has('mpp')) {
|
|
214
|
-
if (!secrets.stripe || !secrets.mppHmacKeyFile)
|
|
215
|
-
throw new CommerceError('mpp_configuration', 'MPP requires private Stripe credentials and a separate challenge key');
|
|
216
|
-
const { MppGate } = await import('./mpp.js'), { stripeProvider } = await import('./stripe-provider.js');
|
|
217
|
-
const providers = Object.fromEntries(Object.entries(secrets.stripe).map(([ref, p]) => [ref, stripeProvider({ ...p, secretKey: privateFile(p.secretKeyFile).toString('utf8').trim() })]));
|
|
218
|
-
const hmacKey = privateFile(secrets.mppHmacKeyFile);
|
|
219
|
-
if (hmacKey.length < 32)
|
|
220
|
-
throw new CommerceError('mpp_hmac_key', 'MPP requires a private challenge key of at least32 random bytes');
|
|
221
|
-
const gate = new MppGate({ store, vault: vault, origin: values.origin, hmacSecret: hmacKey.toString('base64'), providers });
|
|
222
|
-
await gate.check();
|
|
223
|
-
gates.mpp = gate;
|
|
224
|
-
stripeRecipientFor = ref => gate.merchantRecipient(ref);
|
|
193
|
+
let balanceGate;
|
|
194
|
+
if (needsBalance) {
|
|
195
|
+
if (!integration)
|
|
196
|
+
throw new CommerceError('balance_configuration', 'Envar balance requires an authenticated Envar seller integration');
|
|
197
|
+
balanceGate = new EnvarBalanceGate({ store, vault: vault, claim: args => integration.claimBalancePayment(args) });
|
|
225
198
|
}
|
|
226
|
-
const
|
|
199
|
+
const nativeSkills = secrets.skills ? createNativeSkillExecutor(secrets.skills, values.origin, config.agent.name) : undefined;
|
|
200
|
+
const discovery = secrets.skills?.discovery?.enabledFromWeb ? new SkillDiscovery(secrets.skills, secrets.skills.discovery, values.origin) : undefined;
|
|
201
|
+
if (nativeSkills)
|
|
202
|
+
closers.push(() => nativeSkills.close());
|
|
203
|
+
const upstreams = new PinnedUpstreams(join(dirname(values.state), 'upstream-bindings.json'), secrets.upstreams ?? {}, secrets.upstreamInputEncoding, fetch, controller.signal, nativeSkills?.execute);
|
|
227
204
|
upstreams.prepare(config, store.catalogHistory().map(entry => entry.service));
|
|
228
205
|
const validateAdapters = (candidate) => {
|
|
229
206
|
for (const profile of Object.values(candidate.paymentProfiles)) {
|
|
230
|
-
if (!
|
|
231
|
-
throw new CommerceError('
|
|
232
|
-
if (profile.adapter === 'x402' && !secrets.rpcUrls?.[profile.network])
|
|
233
|
-
throw new CommerceError('x402_configuration', 'Configure the required network RPC locally');
|
|
234
|
-
if (profile.adapter === 'mpp' && !stripeRecipientFor?.(profile.accountRef))
|
|
235
|
-
throw new CommerceError('mpp_configuration', 'Configure and verify the merchant account locally');
|
|
207
|
+
if (!balanceGate || !syncSettings?.policy.allowedBalanceAccounts?.includes(profile.accountRef))
|
|
208
|
+
throw new CommerceError('balance_configuration', 'Allow this balance receiving account locally');
|
|
236
209
|
}
|
|
237
210
|
};
|
|
238
211
|
validateAdapters(config);
|
|
239
|
-
const server = new CommerceServer({ config, origin: values.origin, store,
|
|
212
|
+
const server = new CommerceServer({ config, origin: values.origin, store, ownershipChallenges: secrets.ownershipChallenges,
|
|
240
213
|
authenticate: bearerAuthenticator(secrets.callers), execute: upstreams.execute,
|
|
241
214
|
paymentGate: { handle: async (request, order) => {
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
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);
|
|
246
218
|
} }, onError: (code, orderId) => console.error(JSON.stringify({ code, orderId })) });
|
|
247
219
|
closers.push(async () => { server.stop(); controller.abort(); while (server.isRunning)
|
|
248
220
|
await new Promise(r => setTimeout(r, 50)); });
|
|
249
221
|
let connection;
|
|
250
222
|
if (integration) {
|
|
251
|
-
connection = new EnvarSellerRuntime({ integration, server, upstreams, pollIntervalMs: syncSettings?.pollIntervalMs, validateConfig: validateAdapters, onError: code => console.error(JSON.stringify({ code, integration: 'envar' })) });
|
|
223
|
+
connection = new EnvarSellerRuntime({ integration, server, upstreams, pollIntervalMs: syncSettings?.pollIntervalMs, validateConfig: validateAdapters, beforeSync: discovery ? () => discovery.sync(integration) : undefined, onError: code => console.error(JSON.stringify({ code, integration: 'envar' })) });
|
|
252
224
|
closers.push(() => connection.stop());
|
|
253
225
|
}
|
|
254
226
|
const http = listenCommerce(server, values.origin, values.host ?? '127.0.0.1', port(4020));
|
|
@@ -256,7 +228,7 @@ async function run() {
|
|
|
256
228
|
await once(http, 'listening');
|
|
257
229
|
connection?.start();
|
|
258
230
|
signals();
|
|
259
|
-
console.log(JSON.stringify({ listening: port(4020), role: 'seller',
|
|
231
|
+
console.log(JSON.stringify({ listening: port(4020), role: 'seller', paymentMethod: needsBalance ? 'envar_balance' : 'free' }));
|
|
260
232
|
}
|
|
261
233
|
try {
|
|
262
234
|
await run();
|
|
@@ -1,7 +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
5
|
export declare function digest(value: unknown): string;
|
|
6
|
+
/** Keep legacy quotes unchanged; skill-bound quotes freeze the installed skill revision too. */
|
|
7
|
+
export declare function serviceTermsDigest(configVersion: 1 | 2, service: Service, offer: Offer, profile: PaymentProfile | null): string;
|
|
6
8
|
export declare function loadCommerceConfig(value: unknown): CommerceConfig;
|
|
7
|
-
export declare function loadBuyerPolicy(value: unknown): BuyerPolicy;
|
package/dist/commerce/config.js
CHANGED
|
@@ -10,11 +10,6 @@ const ajv = new Ajv2020({ allErrors: true, strict: true });
|
|
|
10
10
|
addFormats(ajv);
|
|
11
11
|
const schema = (name) => JSON.parse(readFileSync(new URL(`../../schemas/${name}`, import.meta.url), 'utf8'));
|
|
12
12
|
const sellerValidator = ajv.compile(schema('envarpay.config.schema.json'));
|
|
13
|
-
const buyerValidator = ajv.compile(schema('buyer-policy.schema.json'));
|
|
14
|
-
const USDC = {
|
|
15
|
-
'eip155:84532': '0x036cbd53842c5426634e7929541ec2318f3dcf7e',
|
|
16
|
-
'eip155:8453': '0x833589fcd6edb6e08f4c7c32d4f71b54bda02913',
|
|
17
|
-
};
|
|
18
13
|
export function atomic(value) {
|
|
19
14
|
if (typeof value !== 'string' || !/^[1-9][0-9]{0,77}$/.test(value))
|
|
20
15
|
throw new CommerceError('invalid_amount', 'Use a positive integer string in smallest currency units');
|
|
@@ -43,7 +38,7 @@ export function validateUrl(value, allowPrivate = false) {
|
|
|
43
38
|
return url;
|
|
44
39
|
}
|
|
45
40
|
export function currencyOf(profile) {
|
|
46
|
-
return profile.
|
|
41
|
+
return profile.currency;
|
|
47
42
|
}
|
|
48
43
|
function assertJson(value) {
|
|
49
44
|
if (typeof value === 'number' && (!Number.isFinite(value) || (Number.isInteger(value) && !Number.isSafeInteger(value))))
|
|
@@ -64,22 +59,29 @@ export function digest(value) {
|
|
|
64
59
|
throw new CommerceError('invalid_json', 'Cannot canonicalize input');
|
|
65
60
|
return createHash('sha256').update(serialized).digest('hex');
|
|
66
61
|
}
|
|
62
|
+
/** Keep legacy quotes unchanged; skill-bound quotes freeze the installed skill revision too. */
|
|
63
|
+
export function serviceTermsDigest(configVersion, service, offer, profile) {
|
|
64
|
+
const terms = { contract: service.contract, offer, profile };
|
|
65
|
+
return digest(configVersion === 2 && service.execution.type === 'skill'
|
|
66
|
+
? { ...terms, skill: { name: service.id, digest: service.execution.skillDigest } }
|
|
67
|
+
: terms);
|
|
68
|
+
}
|
|
67
69
|
export function loadCommerceConfig(value) {
|
|
68
70
|
if (!sellerValidator(value))
|
|
69
71
|
throw new CommerceError('invalid_config', ajv.errorsText(sellerValidator.errors));
|
|
70
72
|
const config = structuredClone(value);
|
|
71
73
|
const services = new Set();
|
|
72
|
-
for (const profile of Object.values(config.paymentProfiles)) {
|
|
73
|
-
if (profile.adapter === 'x402') {
|
|
74
|
-
if (USDC[profile.network] !== profile.asset.toLowerCase() || BigInt(profile.payTo) === 0n)
|
|
75
|
-
throw new CommerceError('unsupported_payment_profile', 'MVP accepts official Base or Base Sepolia USDC and a nonzero receiving address');
|
|
76
|
-
validateUrl(profile.facilitatorUrl);
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
74
|
for (const service of config.services) {
|
|
80
75
|
if (services.has(service.id))
|
|
81
76
|
throw new CommerceError('duplicate_service', 'Service IDs must be unique');
|
|
82
77
|
services.add(service.id);
|
|
78
|
+
if (config.configVersion === 2 && service.execution.type === 'skill') {
|
|
79
|
+
if (service.id !== service.name)
|
|
80
|
+
throw new CommerceError('skill_name_mismatch', 'A version 2 service must use the exact installed skill name as its ID and name');
|
|
81
|
+
}
|
|
82
|
+
else if (config.configVersion === 1 && service.execution.type !== 'a2a') {
|
|
83
|
+
throw new CommerceError('legacy_skill_mode', 'Skill-bound services require configVersion 2');
|
|
84
|
+
}
|
|
83
85
|
validateUrl(service.execution.cardUrl, true);
|
|
84
86
|
noExternalRefs(service.contract.inputSchema);
|
|
85
87
|
const inputAjv = new Ajv2020({ strict: true });
|
|
@@ -95,9 +97,9 @@ export function loadCommerceConfig(value) {
|
|
|
95
97
|
const profile = config.paymentProfiles[offer.paymentProfile];
|
|
96
98
|
if (!profile)
|
|
97
99
|
throw new CommerceError('missing_payment_profile', 'Offer refers to an unknown payment profile');
|
|
98
|
-
let
|
|
100
|
+
let high;
|
|
99
101
|
if (offer.pricing.kind === 'fixed')
|
|
100
|
-
|
|
102
|
+
high = atomic(offer.pricing.amount);
|
|
101
103
|
else {
|
|
102
104
|
const q = offer.pricing.quantity;
|
|
103
105
|
if (q.min > q.max)
|
|
@@ -110,73 +112,13 @@ export function loadCommerceConfig(value) {
|
|
|
110
112
|
}
|
|
111
113
|
if (node.type !== 'array' || q.min < Number(node.minItems ?? 0) || q.max > Number(node.maxItems ?? 10000))
|
|
112
114
|
throw new CommerceError('invalid_quantity', 'Quantity must reference a bounded declared array');
|
|
113
|
-
low = atomic(offer.pricing.unitAmount) * BigInt(q.min);
|
|
114
115
|
high = atomic(offer.pricing.unitAmount) * BigInt(q.max);
|
|
115
116
|
}
|
|
116
117
|
if (high >= 2n ** 256n)
|
|
117
118
|
throw new CommerceError('invalid_amount', 'Maximum total exceeds supported integer range');
|
|
118
|
-
if (
|
|
119
|
-
throw new CommerceError('
|
|
119
|
+
if (high > 2n ** 63n - 1n)
|
|
120
|
+
throw new CommerceError('balance_amount', 'Envar balance prices must fit signed micro-USD');
|
|
120
121
|
}
|
|
121
122
|
}
|
|
122
123
|
return config;
|
|
123
124
|
}
|
|
124
|
-
export function loadBuyerPolicy(value) {
|
|
125
|
-
if (!buyerValidator(value))
|
|
126
|
-
throw new CommerceError('invalid_policy', ajv.errorsText(buyerValidator.errors));
|
|
127
|
-
const policy = structuredClone(value);
|
|
128
|
-
const normalizedCurrency = (currency) => {
|
|
129
|
-
if (currency === 'usd')
|
|
130
|
-
return currency;
|
|
131
|
-
const match = /^(eip155:[1-9][0-9]*)\/erc20:(0x[0-9a-fA-F]{40})$/.exec(currency);
|
|
132
|
-
if (!match || USDC[match[1]] !== match[2].toLowerCase())
|
|
133
|
-
throw new CommerceError('unsupported_currency', 'Buyer budget must use USD or an official supported USDC asset');
|
|
134
|
-
return `${match[1]}/erc20:${match[2].toLowerCase()}`;
|
|
135
|
-
};
|
|
136
|
-
const limits = new Map();
|
|
137
|
-
for (const b of policy.budgets) {
|
|
138
|
-
b.currency = normalizedCurrency(b.currency);
|
|
139
|
-
if (limits.has(b.currency))
|
|
140
|
-
throw new CommerceError('duplicate_budget', 'Only one budget per currency is allowed');
|
|
141
|
-
limits.set(b.currency, atomic(b.maxTotal));
|
|
142
|
-
}
|
|
143
|
-
const ids = new Set();
|
|
144
|
-
for (const peer of policy.peers) {
|
|
145
|
-
validateUrl(peer.cardUrl);
|
|
146
|
-
if (peer.mode === 'standard-a2a') {
|
|
147
|
-
const endpoint = validateUrl(peer.endpoint), card = validateUrl(peer.cardUrl);
|
|
148
|
-
if (endpoint.origin !== card.origin || endpoint.search || endpoint.href !== peer.endpoint || card.href !== peer.cardUrl)
|
|
149
|
-
throw new CommerceError('standard_peer_endpoint', 'Review an exact same-origin HTTPS A2A endpoint without query parameters');
|
|
150
|
-
noExternalRefs(peer.localContract.inputSchema);
|
|
151
|
-
if (JSON.stringify(peer.localContract.inputSchema).length > 512 * 1024)
|
|
152
|
-
throw new CommerceError('standard_peer_schema', 'Local input schema exceeds 512 KiB');
|
|
153
|
-
try {
|
|
154
|
-
const inputAjv = new Ajv2020({ strict: true });
|
|
155
|
-
addFormats(inputAjv);
|
|
156
|
-
inputAjv.compile(peer.localContract.inputSchema);
|
|
157
|
-
}
|
|
158
|
-
catch {
|
|
159
|
-
throw new CommerceError('standard_peer_schema', 'Local input schema must be a valid supported JSON Schema');
|
|
160
|
-
}
|
|
161
|
-
if (atomic(peer.localContract.amount) > atomic(peer.maxPerPurchase))
|
|
162
|
-
throw new CommerceError('purchase_limit', 'Locally reviewed standard-peer price exceeds the per-purchase limit');
|
|
163
|
-
}
|
|
164
|
-
if (ids.has(peer.id))
|
|
165
|
-
throw new CommerceError('duplicate_peer', 'Peer IDs must be unique');
|
|
166
|
-
ids.add(peer.id);
|
|
167
|
-
if (peer.protocol === 'free')
|
|
168
|
-
continue;
|
|
169
|
-
peer.currency = normalizedCurrency(peer.currency);
|
|
170
|
-
if (peer.protocol === 'x402') {
|
|
171
|
-
if (!/^0x[0-9a-fA-F]{40}$/.test(peer.recipient) || BigInt(peer.recipient) === 0n || peer.currency === 'usd')
|
|
172
|
-
throw new CommerceError('invalid_recipient', 'x402 peers require a nonzero EVM recipient and USDC asset');
|
|
173
|
-
peer.recipient = peer.recipient.toLowerCase();
|
|
174
|
-
}
|
|
175
|
-
else if (peer.currency !== 'usd' || !/^profile_[A-Za-z0-9]+$/.test(peer.recipient))
|
|
176
|
-
throw new CommerceError('invalid_recipient', 'MPP peers require USD and a verified Stripe merchant profile');
|
|
177
|
-
const limit = limits.get(peer.currency);
|
|
178
|
-
if (limit === undefined || atomic(peer.maxPerPurchase) > limit)
|
|
179
|
-
throw new CommerceError('invalid_budget', 'Each peer needs a cumulative budget at least as large as its per-purchase limit');
|
|
180
|
-
}
|
|
181
|
-
return policy;
|
|
182
|
-
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, lstatSync, existsSync, renameSync, } from "node:fs";
|
|
2
|
+
import { join, resolve, dirname } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { CommerceError } from "./types.js";
|
|
5
|
+
function read(path) {
|
|
6
|
+
const s = lstatSync(path);
|
|
7
|
+
if (!s.isFile() ||
|
|
8
|
+
s.isSymbolicLink() ||
|
|
9
|
+
s.size > 1024 * 1024 ||
|
|
10
|
+
s.mode & 0o077)
|
|
11
|
+
throw new CommerceError("private_file", "Use an owner-only configuration file");
|
|
12
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
13
|
+
}
|
|
14
|
+
function save(path, value) {
|
|
15
|
+
const temp = path + ".tmp";
|
|
16
|
+
writeFileSync(temp, JSON.stringify(value, null, 2) + "\n", {
|
|
17
|
+
mode: 0o600,
|
|
18
|
+
flag: "wx",
|
|
19
|
+
});
|
|
20
|
+
renameSync(temp, path);
|
|
21
|
+
}
|
|
22
|
+
export function refreshLauncher(directory) {
|
|
23
|
+
const root = resolve(directory), connection = read(join(root, "connection.json")), settingsPath = join(root, "runtime.json");
|
|
24
|
+
// alpha.10 start scripts recorded the port in generated source. Parse JSON argv only; never evaluate code.
|
|
25
|
+
let port;
|
|
26
|
+
if (existsSync(settingsPath))
|
|
27
|
+
port = read(settingsPath).port;
|
|
28
|
+
else {
|
|
29
|
+
const old = readFileSync(join(root, "start.mjs"), "utf8");
|
|
30
|
+
const array = /spawn\([^\n]*?,(\[[^\n]+\]),\{stdio:/.exec(old)?.[1];
|
|
31
|
+
if (!array)
|
|
32
|
+
throw new CommerceError("setup_upgrade", "Cannot infer original start settings. Use serve with original files.");
|
|
33
|
+
const args = JSON.parse(array);
|
|
34
|
+
port = Number(args[args.indexOf("--port") + 1]);
|
|
35
|
+
}
|
|
36
|
+
if (!Number.isInteger(port) || port < 1024 || port > 65535)
|
|
37
|
+
throw new CommerceError("setup_port", "Invalid local service port");
|
|
38
|
+
const args = [
|
|
39
|
+
fileURLToPath(new URL("./cli.js", import.meta.url)),
|
|
40
|
+
"serve",
|
|
41
|
+
"--config",
|
|
42
|
+
join(root, "seller.json"),
|
|
43
|
+
"--credentials",
|
|
44
|
+
join(root, "credentials.json"),
|
|
45
|
+
"--state",
|
|
46
|
+
join(root, "seller.sqlite3"),
|
|
47
|
+
"--origin",
|
|
48
|
+
new URL(connection.url).origin,
|
|
49
|
+
"--host",
|
|
50
|
+
"127.0.0.1",
|
|
51
|
+
"--port",
|
|
52
|
+
String(port),
|
|
53
|
+
"--envar-config",
|
|
54
|
+
join(root, "envar.json"),
|
|
55
|
+
"--envar-credentials",
|
|
56
|
+
join(root, "envar.token"),
|
|
57
|
+
];
|
|
58
|
+
save(settingsPath, { port });
|
|
59
|
+
const start = join(root, "start.mjs");
|
|
60
|
+
writeFileSync(start, `import {spawn} from 'node:child_process';\nconst child=spawn(${JSON.stringify(process.execPath)},${JSON.stringify(args)},{stdio:'inherit'});\nfor(const signal of ['SIGTERM','SIGINT'])process.on(signal,()=>child.kill(signal));\nchild.on('exit',code=>process.exit(code??1));\n`, { mode: 0o700 });
|
|
61
|
+
}
|
|
62
|
+
export function upgradeDiscovery(directory, home) {
|
|
63
|
+
const root = resolve(directory), file = join(root, "credentials.json"), credentials = read(file);
|
|
64
|
+
if (credentials.skills?.framework !== "hermes")
|
|
65
|
+
throw new CommerceError("setup_framework", "This discovery setup supports Hermes");
|
|
66
|
+
const skill = credentials.skills;
|
|
67
|
+
const hermesHome = home ??
|
|
68
|
+
skill.discovery?.hermesHome ??
|
|
69
|
+
(skill.dockerStateDirectory
|
|
70
|
+
? dirname(dirname(skill.dockerStateDirectory))
|
|
71
|
+
: undefined);
|
|
72
|
+
if (!hermesHome)
|
|
73
|
+
throw new CommerceError("setup_home", "Pass --hermes-home with your existing Hermes profile directory");
|
|
74
|
+
credentials.skills.discovery = { hermesHome, enabledFromWeb: true };
|
|
75
|
+
refreshLauncher(root);
|
|
76
|
+
save(file, credentials);
|
|
77
|
+
console.log(`Discovery enabled. Stop the original service with Ctrl+C, then restart:\n${process.execPath} ${JSON.stringify(join(root, "start.mjs"))}\nInstalled Skills will appear privately in Envar. No service is automatically enabled or published.`);
|
|
78
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { NativeSkillSettings } from "./native-skills.js";
|
|
2
|
+
import type { EnvarIntegration } from "./envar.js";
|
|
3
|
+
export interface DiscoverySettings {
|
|
4
|
+
hermesHome: string;
|
|
5
|
+
enabledFromWeb: true;
|
|
6
|
+
roots?: string[];
|
|
7
|
+
}
|
|
8
|
+
export declare class SkillDiscovery {
|
|
9
|
+
private settings;
|
|
10
|
+
private discovery;
|
|
11
|
+
private origin;
|
|
12
|
+
private enabled;
|
|
13
|
+
private cached;
|
|
14
|
+
private nextScan;
|
|
15
|
+
private state;
|
|
16
|
+
readonly path: string;
|
|
17
|
+
constructor(settings: NativeSkillSettings, discovery: DiscoverySettings, origin: string);
|
|
18
|
+
private scan;
|
|
19
|
+
sync(integration: EnvarIntegration): Promise<void>;
|
|
20
|
+
}
|