@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
@@ -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, loadBuyerPolicy, digest } from './config.js';
9
+ import { loadCommerceConfig, digest } from './config.js';
12
10
  import { createOfferCard } from './card.js';
13
11
  import { CommerceStore } from './store.js';
14
12
  import { CommerceServer, bearerAuthenticator } from './server.js';
@@ -16,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 { X402Gate } from './x402.js';
20
- import { evmReceiptVerifier, evmSettlementRecovery } from './chain.js';
17
+ import { EnvarBalanceGate } from './envar-balance.js';
21
18
  import { CommerceError } from './types.js';
22
- import { inspectLedger } from './operations.js';
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/buyer-serve --config FILE --credentials FILE --state DB --origin URL';
34
+ const usage = 'envarpay setup --file envar-setup.json [--docker-container NAME | --python PATH] [--hermes-home PATH] [--directory DIR]\nenvarpay onboard --file invitation.json [--framework hermes|openclaw] [--check] [--python PATH] [--docker-container NAME] [--hermes-home PATH] [--origin HTTPS_ORIGIN]\nenvarpay upgrade --directory DIR [--hermes-home PATH]\nenvarpay external-setup --directory DIR --origin HTTPS_ORIGIN | external-serve --config FILE --credentials FILE --state DB --origin HTTPS_ORIGIN\nenvarpay init --directory DIR | inspect --state DB [--after ROW --limit 100 --refund-review] | validate --config seller.json | card --config seller.json --service ID --offer ID --origin URL | serve --config FILE --credentials FILE --state DB --origin URL';
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'], ['buyer-policy.json', 'buyer-policy.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 protocols = new Set(Object.values(config.paymentProfiles).map(p => p.adapter));
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 (protocols.size) {
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
- const gates = {};
205
- let stripeRecipientFor;
206
- if (protocols.has('x402')) {
207
- if (!secrets.payers || !secrets.rpcUrls)
208
- throw new CommerceError('x402_configuration', 'x402 requires explicit caller-payer bindings and network RPCs');
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 upstreams = new PinnedUpstreams(join(dirname(values.state), 'upstream-bindings.json'), secrets.upstreams, secrets.upstreamInputEncoding, fetch, controller.signal);
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 (!gates[profile.adapter])
231
- throw new CommerceError('payment_method_unavailable', 'Configure the payment adapter locally before enabling this service');
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, stripeRecipientFor, ownershipChallenges: secrets.ownershipChallenges,
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
- const adapter = order.quote.paymentProfile?.adapter, gate = adapter ? gates[adapter] : undefined;
243
- if (!gate)
244
- throw new CommerceError('payment_method_unavailable', 'Configured payment adapter is unavailable');
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', paidTransports: [...protocols] }));
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 BuyerPolicy, type CommerceConfig, type PaymentProfile } from './types.js';
1
+ import { type CommerceConfig, type Offer, type PaymentProfile, type Service } from './types.js';
2
2
  export declare function atomic(value: string): bigint;
3
3
  export declare function validateUrl(value: string, allowPrivate?: boolean): URL;
4
4
  export declare function currencyOf(profile: PaymentProfile): string;
5
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;
@@ -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.adapter === 'x402' ? `${profile.network}/erc20:${profile.asset.toLowerCase()}` : profile.currency;
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 low, high;
100
+ let high;
99
101
  if (offer.pricing.kind === 'fixed')
100
- low = high = atomic(offer.pricing.amount);
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 (profile.adapter === 'mpp' && low < 50n)
119
- throw new CommerceError('card_minimum', 'USD card orders must be at least 50 cents');
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,2 @@
1
+ export declare function refreshLauncher(directory: string): void;
2
+ export declare function upgradeDiscovery(directory: string, home?: string): void;
@@ -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
+ }