@envarai/envarpay 0.2.0-alpha.9 → 2026.1025.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. package/README.md +18 -56
  2. package/dist/commerce/agent-setup.d.ts +19 -0
  3. package/dist/commerce/agent-setup.js +210 -0
  4. package/dist/commerce/alerts.d.ts +5 -15
  5. package/dist/commerce/alerts.js +21 -77
  6. package/dist/commerce/card.js +1 -1
  7. package/dist/commerce/cli.js +74 -137
  8. package/dist/commerce/config.d.ts +1 -4
  9. package/dist/commerce/config.js +5 -81
  10. package/dist/commerce/configure.d.ts +2 -0
  11. package/dist/commerce/configure.js +78 -0
  12. package/dist/commerce/discovery.d.ts +20 -0
  13. package/dist/commerce/discovery.js +162 -0
  14. package/dist/commerce/envar-balance.d.ts +25 -0
  15. package/dist/commerce/envar-balance.js +65 -0
  16. package/dist/commerce/envar-runtime.d.ts +1 -0
  17. package/dist/commerce/envar-runtime.js +7 -0
  18. package/dist/commerce/envar.d.ts +13 -31
  19. package/dist/commerce/envar.js +35 -83
  20. package/dist/commerce/external-agent.d.ts +26 -0
  21. package/dist/commerce/external-agent.js +129 -0
  22. package/dist/commerce/external-events.d.ts +76 -0
  23. package/dist/commerce/external-events.js +273 -0
  24. package/dist/commerce/external-mcp.d.ts +10 -0
  25. package/dist/commerce/external-mcp.js +199 -0
  26. package/dist/commerce/external-oauth.d.ts +12 -0
  27. package/dist/commerce/external-oauth.js +230 -0
  28. package/dist/commerce/external-runtime.d.ts +25 -0
  29. package/dist/commerce/external-runtime.js +244 -0
  30. package/dist/commerce/managed.d.ts +3 -0
  31. package/dist/commerce/managed.js +143 -0
  32. package/dist/commerce/native-inventory.d.ts +9 -0
  33. package/dist/commerce/native-inventory.js +75 -0
  34. package/dist/commerce/native-skills.d.ts +8 -0
  35. package/dist/commerce/native-skills.js +11 -8
  36. package/dist/commerce/openclaw-setup.d.ts +28 -0
  37. package/dist/commerce/openclaw-setup.js +43 -0
  38. package/dist/commerce/operations.d.ts +1 -2
  39. package/dist/commerce/operations.js +17 -20
  40. package/dist/commerce/pricing.d.ts +0 -1
  41. package/dist/commerce/pricing.js +1 -3
  42. package/dist/commerce/runtime.d.ts +2 -2
  43. package/dist/commerce/runtime.js +2 -2
  44. package/dist/commerce/server.d.ts +0 -1
  45. package/dist/commerce/server.js +2 -3
  46. package/dist/commerce/setup.d.ts +25 -0
  47. package/dist/commerce/setup.js +379 -0
  48. package/dist/commerce/store.d.ts +11 -1
  49. package/dist/commerce/store.js +36 -2
  50. package/dist/commerce/types.d.ts +1 -56
  51. package/examples/envar-integration-policy.json +1 -9
  52. package/examples/seller.json +143 -26
  53. package/package.json +11 -20
  54. package/runtime/hermes-setup-probe.py +109 -0
  55. package/runtime/hermes-skill-inventory.py +108 -0
  56. package/schemas/envarpay.config.schema.json +7 -73
  57. package/dist/commerce/buyer-store.d.ts +0 -101
  58. package/dist/commerce/buyer-store.js +0 -168
  59. package/dist/commerce/buyer.d.ts +0 -138
  60. package/dist/commerce/buyer.js +0 -775
  61. package/dist/commerce/chain.d.ts +0 -24
  62. package/dist/commerce/chain.js +0 -146
  63. package/dist/commerce/client-runtime.d.ts +0 -8
  64. package/dist/commerce/client-runtime.js +0 -7
  65. package/dist/commerce/management.d.ts +0 -14
  66. package/dist/commerce/management.js +0 -140
  67. package/dist/commerce/mpp-client.d.ts +0 -70
  68. package/dist/commerce/mpp-client.js +0 -29
  69. package/dist/commerce/mpp-runtime.d.ts +0 -4
  70. package/dist/commerce/mpp-runtime.js +0 -4
  71. package/dist/commerce/mpp.d.ts +0 -53
  72. package/dist/commerce/mpp.js +0 -163
  73. package/dist/commerce/peer-proxy.d.ts +0 -44
  74. package/dist/commerce/peer-proxy.js +0 -673
  75. package/dist/commerce/stripe-issuer.d.ts +0 -41
  76. package/dist/commerce/stripe-issuer.js +0 -351
  77. package/dist/commerce/stripe-provider.d.ts +0 -11
  78. package/dist/commerce/stripe-provider.js +0 -132
  79. package/dist/commerce/x402.d.ts +0 -38
  80. package/dist/commerce/x402.js +0 -181
  81. package/dist/index.d.ts +0 -95
  82. package/dist/index.js +0 -163
  83. package/examples/buyer-policy.json +0 -18
  84. package/schemas/buyer-policy.schema.json +0 -253
  85. /package/{examples → runtime}/native-skill-runner.py +0 -0
@@ -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,18 +14,23 @@ 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
19
  import { createNativeSkillExecutor } from './native-skills.js';
23
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';
24
26
  const { values, positionals } = parseArgs({ allowPositionals: true, options: {
25
27
  config: { type: 'string' }, service: { type: 'string' }, offer: { type: 'string' }, origin: { type: 'string' },
26
28
  version: { type: 'boolean' }, help: { type: 'boolean', short: 'h' }, state: { type: 'string' }, credentials: { type: 'string' }, host: { type: 'string' },
27
29
  port: { type: 'string' }, directory: { type: 'string' }, 'envar-config': { type: 'string' }, 'envar-credentials': { type: 'string' },
28
30
  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' },
31
+ framework: { type: 'string' }, file: { type: 'string' }, 'docker-container': { type: 'string' }, python: { type: 'string' }, 'hermes-home': { type: 'string' }, check: { type: 'boolean' },
29
32
  } });
30
- const usage = 'envarpay init --directory DIR | inspect --state DB [--after ROW --limit 100 --refund-review] [--alerts --config buyer-policy.json --integration-state DB/--no-integration --fail-on-alert] | 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';
33
+ 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';
31
34
  const closers = [];
32
35
  function privateFile(path) {
33
36
  const stat = lstatSync(path);
@@ -67,16 +70,6 @@ function signals() {
67
70
  async function closeHttp(http) {
68
71
  await new Promise((resolve, reject) => http.close(error => error ? reject(error) : resolve()));
69
72
  }
70
- async function loadMppAdapter(path) {
71
- privateFile(path);
72
- const module = await import(pathToFileURL(resolve(path)).href);
73
- if (typeof module.createMppBuyerOptions !== 'function')
74
- throw new CommerceError('mpp_adapter_module', 'Owner-supplied module must export createMppBuyerOptions');
75
- const options = await module.createMppBuyerOptions();
76
- if (!options || typeof options.createToken !== 'function' || typeof options.recoverToken !== 'function' || typeof options.verifyReceipt !== 'function')
77
- throw new CommerceError('mpp_adapter_module', 'MPP requires real token creation, original-operation recovery and receipt verification');
78
- return options;
79
- }
80
73
  async function run() {
81
74
  if (values.help || positionals.length === 0 && !values.version) {
82
75
  console.log(usage);
@@ -87,10 +80,52 @@ async function run() {
87
80
  return;
88
81
  }
89
82
  const command = positionals[0];
83
+ if (command === 'external-setup') {
84
+ const { setupExternalAgent } = await import('./external-runtime.js');
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
+ const { serveExternalAgent } = await import('./external-runtime.js');
92
+ if (!values.config || !values.credentials || !values.state || !values.origin)
93
+ throw new CommerceError('external_serve', 'Provide config, credentials, state and origin');
94
+ 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) });
95
+ closers.push(() => runtime.close());
96
+ await once(runtime.http, 'listening');
97
+ signals();
98
+ console.log(JSON.stringify({ listening: port(4020), role: 'external-agent', paymentsEnabled: false }));
99
+ return;
100
+ }
101
+ if (values.framework && !['hermes', 'openclaw'].includes(values.framework))
102
+ throw new CommerceError('setup_runtime', 'Use hermes or openclaw');
103
+ if (command === 'managed-serve') {
104
+ if (!values.directory || !values.origin)
105
+ throw new CommerceError('managed_configuration', 'Managed serve requires its directory and HTTPS origin');
106
+ await serveManaged(values.directory, values.origin, values.host ?? '127.0.0.1', port(4020));
107
+ return;
108
+ }
109
+ if (command === 'onboard') {
110
+ if (!values.file)
111
+ throw new CommerceError('setup_file', 'Save the private invitation and pass --file PATH.');
112
+ 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 });
113
+ return;
114
+ }
115
+ if (command === 'upgrade' && values.directory) {
116
+ upgradeDiscovery(values.directory, values['hermes-home']);
117
+ return;
118
+ }
119
+ if (command === 'setup') {
120
+ if (!values.file)
121
+ throw new CommerceError('setup_file', 'Download the setup file from your Agent service page and pass --file PATH');
122
+ await setupAgent({ framework: values.framework, file: values.file, directory: values.directory, container: values['docker-container'], python: values.python, home: values['hermes-home'] });
123
+ return;
124
+ }
90
125
  if (values.alerts || values['fail-on-alert'] || values['integration-state'] || values['no-integration']) {
91
126
  if (command !== 'inspect' || !values.state || !values.alerts || values['refund-review'] || values.after || values.limit || (values['no-integration'] && values['integration-state']))
92
127
  throw new CommerceError('inspection_view', 'Alert inspection needs inspect --state and --alerts, without pagination or refund review; choose at most one integration source');
93
- const report = inspectLocalAlerts(values.state, { policyPath: values.config, integrationPath: values['integration-state'], integrationDisabled: values['no-integration'] });
128
+ const report = inspectLocalAlerts(values.state, { integrationPath: values['integration-state'], integrationDisabled: values['no-integration'] });
94
129
  console.log(JSON.stringify(report, null, 2));
95
130
  if (values['fail-on-alert'])
96
131
  process.exitCode = report.recommendedExitCode;
@@ -108,7 +143,7 @@ async function run() {
108
143
  throw new CommerceError('envar_configuration', 'Supply both --envar-config and --envar-credentials');
109
144
  if (command === 'init' && values.directory) {
110
145
  mkdirSync(values.directory, { recursive: true, mode: 0o700 });
111
- for (const [name, source] of [['seller.json', 'seller.json'], ['buyer-policy.json', 'buyer-policy.json']]) {
146
+ for (const [name, source] of [['seller.json', 'seller.json']]) {
112
147
  writeFileSync(join(values.directory, name), readFileSync(new URL(`../../examples/${source}`, import.meta.url)), { flag: 'wx', mode: 0o600 });
113
148
  }
114
149
  writeFileSync(join(values.directory, 'vault.key'), randomBytes(32), { flag: 'wx', mode: 0o600, flush: true });
@@ -118,81 +153,6 @@ async function run() {
118
153
  if (!values.config)
119
154
  throw new CommerceError('usage', usage);
120
155
  const raw = JSON.parse(readFileSync(values.config, 'utf8'));
121
- if (command === 'buyer-serve') {
122
- if (!values.origin || !values.state || !values.credentials)
123
- throw new CommerceError('usage', usage);
124
- const policy = loadBuyerPolicy(raw);
125
- const secrets = JSON.parse(privateFile(values.credentials).toString('utf8'));
126
- const needsEvm = policy.peers.some(peer => peer.protocol === 'x402'), needsMpp = policy.peers.some(peer => peer.protocol === 'mpp');
127
- if (!secrets.peerTokens || (needsEvm || needsMpp) && !secrets.vaultKeyFile)
128
- throw new CommerceError('buyer_credentials', 'Paid buyer needs private vault key and peer credentials');
129
- let signer;
130
- if (needsEvm) {
131
- if (!secrets.privateKeyFile || !secrets.rpcUrls)
132
- throw new CommerceError('buyer_credentials', 'x402 peers require local signer and RPC configuration');
133
- const key = privateFile(secrets.privateKeyFile).toString('utf8').trim();
134
- if (!/^0x[0-9a-fA-F]{64}$/.test(key))
135
- throw new CommerceError('buyer_key', 'Signer file must contain one 32-byte 0x-prefixed private key');
136
- signer = privateKeyToAccount(key);
137
- }
138
- if (secrets.mppAdapterModule && secrets.mppStripe)
139
- throw new CommerceError('mpp_configuration', 'Choose the built-in Stripe issuer or one explicit adapter module');
140
- if (needsMpp && !secrets.mppAdapterModule && !secrets.mppStripe)
141
- throw new CommerceError('mpp_configuration', 'MPP peers require a configured Stripe issuer or explicit authorized funding adapter');
142
- const buyerVault = secrets.vaultKeyFile ? new CredentialVault(join(dirname(values.state), 'buyer-authorizations'), privateFile(secrets.vaultKeyFile)) : undefined;
143
- let mpp;
144
- if (needsMpp && secrets.mppStripe) {
145
- const configured = secrets.mppStripe;
146
- if (!configured.secretKeyFile || !configured.sellers || typeof configured.sellers !== 'object' || Array.isArray(configured.sellers))
147
- throw new CommerceError('mpp_configuration', 'Stripe issuer and seller read credentials must use private key files');
148
- const { createStripeMppBuyer } = await import('./stripe-issuer.js');
149
- const adapter = createStripeMppBuyer({
150
- payer: configured.payer, mode: configured.mode, issuerAccountId: configured.issuerAccountId,
151
- secretKey: privateFile(configured.secretKeyFile).toString('utf8').trim(), paymentMethod: configured.paymentMethod,
152
- ...(configured.publishableKey !== undefined ? { publishableKey: configured.publishableKey } : {}),
153
- sellers: Object.fromEntries(Object.entries(configured.sellers).map(([id, seller]) => [id, { accountId: seller.accountId, secretKey: privateFile(seller.secretKeyFile).toString('utf8').trim() }])),
154
- ...(configured.issuance ? { issuance: configured.issuance } : {}), ...(configured.returnUrl ? { returnUrl: configured.returnUrl } : {}),
155
- }, { statePath: values.state + '.stripe-issuer.sqlite3', vault: buyerVault });
156
- closers.push(() => adapter.close());
157
- await adapter.assertReady();
158
- mpp = adapter;
159
- }
160
- else if (needsMpp)
161
- mpp = await loadMppAdapter(secrets.mppAdapterModule);
162
- const { BuyerStore } = await import('./buyer-store.js');
163
- const { CommerceBuyer } = await import('./buyer.js');
164
- const { BuyerManagement, listenBuyerManagement } = await import('./management.js');
165
- const store = new BuyerStore(values.state);
166
- closers.push(() => store.close());
167
- const buyer = new CommerceBuyer({ policy, store, vault: buyerVault,
168
- signer, mpp, peerTokens: secrets.peerTokens, peerEndpoints: secrets.peerEndpoints,
169
- ...(needsEvm ? { verifyReceipt: evmReceiptVerifier(secrets.rpcUrls), ...evmSettlementRecovery(secrets.rpcUrls) } : {}) });
170
- closers.push(() => buyer.stop());
171
- if (secrets.peerProxies?.length) {
172
- if (secrets.peerProxies.length > 8)
173
- throw new CommerceError('proxy_limit', 'Configure at most8 explicit peer proxies per buyer');
174
- const { A2APeerProxy, listenPeerProxy } = await import('./peer-proxy.js');
175
- for (const settings of secrets.peerProxies) {
176
- if (Object.keys(settings.tokens).some(token => Object.hasOwn(secrets.callers, token)))
177
- throw new CommerceError('proxy_token_scope', 'Agent proxy tokens must differ from owner management credentials');
178
- if (!Number.isInteger(settings.port) || settings.port < 1 || settings.port > 65535)
179
- throw new CommerceError('proxy_port', 'Invalid proxy listen port');
180
- const proxy = new A2APeerProxy({ ...settings, buyer });
181
- closers.push(async () => { await proxy.stop(); proxy.close(); });
182
- const listener = listenPeerProxy(proxy, settings.host ?? '127.0.0.1', settings.port);
183
- closers.push(() => closeHttp(listener));
184
- await once(listener, 'listening');
185
- }
186
- }
187
- const management = new BuyerManagement({ buyer, origin: values.origin, authenticate: bearerAuthenticator(secrets.callers) });
188
- const http = listenBuyerManagement(management, values.origin, values.host ?? '127.0.0.1', port(4021));
189
- closers.push(() => closeHttp(http));
190
- await once(http, 'listening');
191
- buyer.start();
192
- signals();
193
- console.log(JSON.stringify({ listening: port(4021), role: 'buyer', paymentsEnabled: policy.paymentsEnabled }));
194
- return;
195
- }
196
156
  let config = loadCommerceConfig(raw);
197
157
  if (command === 'validate') {
198
158
  console.log(JSON.stringify({ valid: true, digest: digest(config), services: config.services.length, paymentPerformed: false }));
@@ -210,6 +170,10 @@ async function run() {
210
170
  syncSettings = JSON.parse(privateFile(values['envar-config']).toString('utf8'));
211
171
  if (!syncSettings?.policy)
212
172
  throw new CommerceError('envar_configuration', 'Envar settings must include an explicit local policy');
173
+ if (secrets.skills?.discovery?.enabledFromWeb) {
174
+ 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);
175
+ syncSettings.policy.allowedServices = [...new Set([...syncSettings.policy.allowedServices, ...installed])];
176
+ }
213
177
  const tokenFile = resolve(values['envar-credentials']);
214
178
  privateFile(tokenFile);
215
179
  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() });
@@ -220,71 +184,44 @@ async function run() {
220
184
  closers.push(() => store.close());
221
185
  if (integration)
222
186
  config = restoreEnvarConfig(config, integration, store.catalogHistory());
223
- const protocols = new Set(Object.values(config.paymentProfiles).map(p => p.adapter));
224
- for (const historical of store.catalogHistory())
225
- for (const profile of Object.values(historical.profiles))
226
- protocols.add(profile.adapter);
227
- if (syncSettings?.policy.allowedX402.length)
228
- protocols.add('x402');
229
- if (syncSettings?.policy.allowedMppAccounts.length)
230
- protocols.add('mpp');
187
+ const needsBalance = Boolean(integration) || Object.values(config.paymentProfiles).length > 0 || store.catalogHistory().some(entry => Object.values(entry.profiles).length > 0);
231
188
  let vault;
232
- if (protocols.size) {
189
+ if (needsBalance) {
233
190
  if (!secrets.vaultKeyFile)
234
191
  throw new CommerceError('vault_key', 'Paid services require a private vault key file');
235
192
  vault = new CredentialVault(join(dirname(values.state), 'authorizations'), privateFile(secrets.vaultKeyFile));
236
193
  }
237
- const gates = {};
238
- let stripeRecipientFor;
239
- if (protocols.has('x402')) {
240
- if (!secrets.payers || !secrets.rpcUrls)
241
- throw new CommerceError('x402_configuration', 'x402 requires explicit caller-payer bindings and network RPCs');
242
- const gate = new X402Gate({ store, vault: vault, payerFor: caller => secrets.payers?.[caller], verifyReceipt: evmReceiptVerifier(secrets.rpcUrls), ...evmSettlementRecovery(secrets.rpcUrls) });
243
- await gate.check(Object.values(config.paymentProfiles).filter((p) => p.adapter === 'x402'));
244
- gates.x402 = gate;
245
- }
246
- if (protocols.has('mpp')) {
247
- if (!secrets.stripe || !secrets.mppHmacKeyFile)
248
- throw new CommerceError('mpp_configuration', 'MPP requires private Stripe credentials and a separate challenge key');
249
- const { MppGate } = await import('./mpp.js'), { stripeProvider } = await import('./stripe-provider.js');
250
- const providers = Object.fromEntries(Object.entries(secrets.stripe).map(([ref, p]) => [ref, stripeProvider({ ...p, secretKey: privateFile(p.secretKeyFile).toString('utf8').trim() })]));
251
- const hmacKey = privateFile(secrets.mppHmacKeyFile);
252
- if (hmacKey.length < 32)
253
- throw new CommerceError('mpp_hmac_key', 'MPP requires a private challenge key of at least32 random bytes');
254
- const gate = new MppGate({ store, vault: vault, origin: values.origin, hmacSecret: hmacKey.toString('base64'), providers });
255
- await gate.check();
256
- gates.mpp = gate;
257
- stripeRecipientFor = ref => gate.merchantRecipient(ref);
194
+ let balanceGate;
195
+ if (needsBalance) {
196
+ if (!integration)
197
+ throw new CommerceError('balance_configuration', 'Envar balance requires an authenticated Envar seller integration');
198
+ balanceGate = new EnvarBalanceGate({ store, vault: vault, claim: args => integration.claimBalancePayment(args) });
258
199
  }
259
200
  const nativeSkills = secrets.skills ? createNativeSkillExecutor(secrets.skills, values.origin, config.agent.name) : undefined;
201
+ const discovery = secrets.skills?.discovery?.enabledFromWeb ? new SkillDiscovery(secrets.skills, secrets.skills.discovery, values.origin) : undefined;
260
202
  if (nativeSkills)
261
203
  closers.push(() => nativeSkills.close());
262
204
  const upstreams = new PinnedUpstreams(join(dirname(values.state), 'upstream-bindings.json'), secrets.upstreams ?? {}, secrets.upstreamInputEncoding, fetch, controller.signal, nativeSkills?.execute);
263
205
  upstreams.prepare(config, store.catalogHistory().map(entry => entry.service));
264
206
  const validateAdapters = (candidate) => {
265
207
  for (const profile of Object.values(candidate.paymentProfiles)) {
266
- if (!gates[profile.adapter])
267
- throw new CommerceError('payment_method_unavailable', 'Configure the payment adapter locally before enabling this service');
268
- if (profile.adapter === 'x402' && !secrets.rpcUrls?.[profile.network])
269
- throw new CommerceError('x402_configuration', 'Configure the required network RPC locally');
270
- if (profile.adapter === 'mpp' && !stripeRecipientFor?.(profile.accountRef))
271
- throw new CommerceError('mpp_configuration', 'Configure and verify the merchant account locally');
208
+ if (!balanceGate || !syncSettings?.policy.allowedBalanceAccounts?.includes(profile.accountRef))
209
+ throw new CommerceError('balance_configuration', 'Allow this balance receiving account locally');
272
210
  }
273
211
  };
274
212
  validateAdapters(config);
275
- const server = new CommerceServer({ config, origin: values.origin, store, stripeRecipientFor, ownershipChallenges: secrets.ownershipChallenges,
213
+ const server = new CommerceServer({ config, origin: values.origin, store, ownershipChallenges: secrets.ownershipChallenges,
276
214
  authenticate: bearerAuthenticator(secrets.callers), execute: upstreams.execute,
277
215
  paymentGate: { handle: async (request, order) => {
278
- const adapter = order.quote.paymentProfile?.adapter, gate = adapter ? gates[adapter] : undefined;
279
- if (!gate)
280
- throw new CommerceError('payment_method_unavailable', 'Configured payment adapter is unavailable');
281
- return gate.handle(request, order);
216
+ if (!balanceGate)
217
+ throw new CommerceError('payment_method_unavailable', 'Envar balance is unavailable');
218
+ return balanceGate.handle(request, order);
282
219
  } }, onError: (code, orderId) => console.error(JSON.stringify({ code, orderId })) });
283
220
  closers.push(async () => { server.stop(); controller.abort(); while (server.isRunning)
284
221
  await new Promise(r => setTimeout(r, 50)); });
285
222
  let connection;
286
223
  if (integration) {
287
- connection = new EnvarSellerRuntime({ integration, server, upstreams, pollIntervalMs: syncSettings?.pollIntervalMs, validateConfig: validateAdapters, onError: code => console.error(JSON.stringify({ code, integration: 'envar' })) });
224
+ 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' })) });
288
225
  closers.push(() => connection.stop());
289
226
  }
290
227
  const http = listenCommerce(server, values.origin, values.host ?? '127.0.0.1', port(4020));
@@ -292,7 +229,7 @@ async function run() {
292
229
  await once(http, 'listening');
293
230
  connection?.start();
294
231
  signals();
295
- console.log(JSON.stringify({ listening: port(4020), role: 'seller', paidTransports: [...protocols] }));
232
+ console.log(JSON.stringify({ listening: port(4020), role: 'seller', paymentMethod: needsBalance ? 'envar_balance' : 'free' }));
296
233
  }
297
234
  try {
298
235
  await run();
@@ -1,11 +1,8 @@
1
- import { type BuyerPolicy, type CommerceConfig, type Offer, type PaymentProfile, type Service } 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
- /** Stripe documents sandbox Business Profile IDs as profile_test_<opaque id>. */
6
- export declare function isStripeProfileId(value: unknown): value is string;
7
5
  export declare function digest(value: unknown): string;
8
6
  /** Keep legacy quotes unchanged; skill-bound quotes freeze the installed skill revision too. */
9
7
  export declare function serviceTermsDigest(configVersion: 1 | 2, service: Service, offer: Offer, profile: PaymentProfile | null): string;
10
8
  export declare function loadCommerceConfig(value: unknown): CommerceConfig;
11
- export declare function loadBuyerPolicy(value: unknown): BuyerPolicy;
@@ -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,11 +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;
47
- }
48
- /** Stripe documents sandbox Business Profile IDs as profile_test_<opaque id>. */
49
- export function isStripeProfileId(value) {
50
- return typeof value === 'string' && /^profile_(?:test_)?[A-Za-z0-9]{1,120}$/.test(value);
41
+ return profile.currency;
51
42
  }
52
43
  function assertJson(value) {
53
44
  if (typeof value === 'number' && (!Number.isFinite(value) || (Number.isInteger(value) && !Number.isSafeInteger(value))))
@@ -80,13 +71,6 @@ export function loadCommerceConfig(value) {
80
71
  throw new CommerceError('invalid_config', ajv.errorsText(sellerValidator.errors));
81
72
  const config = structuredClone(value);
82
73
  const services = new Set();
83
- for (const profile of Object.values(config.paymentProfiles)) {
84
- if (profile.adapter === 'x402') {
85
- if (USDC[profile.network] !== profile.asset.toLowerCase() || BigInt(profile.payTo) === 0n)
86
- throw new CommerceError('unsupported_payment_profile', 'MVP accepts official Base or Base Sepolia USDC and a nonzero receiving address');
87
- validateUrl(profile.facilitatorUrl);
88
- }
89
- }
90
74
  for (const service of config.services) {
91
75
  if (services.has(service.id))
92
76
  throw new CommerceError('duplicate_service', 'Service IDs must be unique');
@@ -113,9 +97,9 @@ export function loadCommerceConfig(value) {
113
97
  const profile = config.paymentProfiles[offer.paymentProfile];
114
98
  if (!profile)
115
99
  throw new CommerceError('missing_payment_profile', 'Offer refers to an unknown payment profile');
116
- let low, high;
100
+ let high;
117
101
  if (offer.pricing.kind === 'fixed')
118
- low = high = atomic(offer.pricing.amount);
102
+ high = atomic(offer.pricing.amount);
119
103
  else {
120
104
  const q = offer.pricing.quantity;
121
105
  if (q.min > q.max)
@@ -128,73 +112,13 @@ export function loadCommerceConfig(value) {
128
112
  }
129
113
  if (node.type !== 'array' || q.min < Number(node.minItems ?? 0) || q.max > Number(node.maxItems ?? 10000))
130
114
  throw new CommerceError('invalid_quantity', 'Quantity must reference a bounded declared array');
131
- low = atomic(offer.pricing.unitAmount) * BigInt(q.min);
132
115
  high = atomic(offer.pricing.unitAmount) * BigInt(q.max);
133
116
  }
134
117
  if (high >= 2n ** 256n)
135
118
  throw new CommerceError('invalid_amount', 'Maximum total exceeds supported integer range');
136
- if (profile.adapter === 'mpp' && low < 50n)
137
- 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');
138
121
  }
139
122
  }
140
123
  return config;
141
124
  }
142
- export function loadBuyerPolicy(value) {
143
- if (!buyerValidator(value))
144
- throw new CommerceError('invalid_policy', ajv.errorsText(buyerValidator.errors));
145
- const policy = structuredClone(value);
146
- const normalizedCurrency = (currency) => {
147
- if (currency === 'usd')
148
- return currency;
149
- const match = /^(eip155:[1-9][0-9]*)\/erc20:(0x[0-9a-fA-F]{40})$/.exec(currency);
150
- if (!match || USDC[match[1]] !== match[2].toLowerCase())
151
- throw new CommerceError('unsupported_currency', 'Buyer budget must use USD or an official supported USDC asset');
152
- return `${match[1]}/erc20:${match[2].toLowerCase()}`;
153
- };
154
- const limits = new Map();
155
- for (const b of policy.budgets) {
156
- b.currency = normalizedCurrency(b.currency);
157
- if (limits.has(b.currency))
158
- throw new CommerceError('duplicate_budget', 'Only one budget per currency is allowed');
159
- limits.set(b.currency, atomic(b.maxTotal));
160
- }
161
- const ids = new Set();
162
- for (const peer of policy.peers) {
163
- validateUrl(peer.cardUrl);
164
- if (peer.mode === 'standard-a2a') {
165
- const endpoint = validateUrl(peer.endpoint), card = validateUrl(peer.cardUrl);
166
- if (endpoint.origin !== card.origin || endpoint.search || endpoint.href !== peer.endpoint || card.href !== peer.cardUrl)
167
- throw new CommerceError('standard_peer_endpoint', 'Review an exact same-origin HTTPS A2A endpoint without query parameters');
168
- noExternalRefs(peer.localContract.inputSchema);
169
- if (JSON.stringify(peer.localContract.inputSchema).length > 512 * 1024)
170
- throw new CommerceError('standard_peer_schema', 'Local input schema exceeds 512 KiB');
171
- try {
172
- const inputAjv = new Ajv2020({ strict: true });
173
- addFormats(inputAjv);
174
- inputAjv.compile(peer.localContract.inputSchema);
175
- }
176
- catch {
177
- throw new CommerceError('standard_peer_schema', 'Local input schema must be a valid supported JSON Schema');
178
- }
179
- if (atomic(peer.localContract.amount) > atomic(peer.maxPerPurchase))
180
- throw new CommerceError('purchase_limit', 'Locally reviewed standard-peer price exceeds the per-purchase limit');
181
- }
182
- if (ids.has(peer.id))
183
- throw new CommerceError('duplicate_peer', 'Peer IDs must be unique');
184
- ids.add(peer.id);
185
- if (peer.protocol === 'free')
186
- continue;
187
- peer.currency = normalizedCurrency(peer.currency);
188
- if (peer.protocol === 'x402') {
189
- if (!/^0x[0-9a-fA-F]{40}$/.test(peer.recipient) || BigInt(peer.recipient) === 0n || peer.currency === 'usd')
190
- throw new CommerceError('invalid_recipient', 'x402 peers require a nonzero EVM recipient and USDC asset');
191
- peer.recipient = peer.recipient.toLowerCase();
192
- }
193
- else if (peer.currency !== 'usd' || !isStripeProfileId(peer.recipient))
194
- throw new CommerceError('invalid_recipient', 'MPP peers require USD and a verified Stripe merchant profile');
195
- const limit = limits.get(peer.currency);
196
- if (limit === undefined || atomic(peer.maxPerPurchase) > limit)
197
- throw new CommerceError('invalid_budget', 'Each peer needs a cumulative budget at least as large as its per-purchase limit');
198
- }
199
- return policy;
200
- }
@@ -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
+ }