@envarai/envarpay 0.2.0-alpha.1 → 0.2.0-alpha.11

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 (55) hide show
  1. package/README.md +2 -0
  2. package/dist/commerce/alerts.d.ts +37 -0
  3. package/dist/commerce/alerts.js +127 -0
  4. package/dist/commerce/buyer-store.d.ts +8 -0
  5. package/dist/commerce/buyer.d.ts +12 -3
  6. package/dist/commerce/buyer.js +151 -7
  7. package/dist/commerce/card.js +2 -2
  8. package/dist/commerce/cli.js +74 -9
  9. package/dist/commerce/client-runtime.d.ts +1 -1
  10. package/dist/commerce/config.d.ts +5 -1
  11. package/dist/commerce/config.js +37 -1
  12. package/dist/commerce/configure.d.ts +3 -0
  13. package/dist/commerce/configure.js +158 -0
  14. package/dist/commerce/discovery.d.ts +23 -0
  15. package/dist/commerce/discovery.js +162 -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 +5 -0
  19. package/dist/commerce/envar.js +13 -0
  20. package/dist/commerce/index.d.ts +1 -0
  21. package/dist/commerce/index.js +1 -0
  22. package/dist/commerce/management.js +3 -1
  23. package/dist/commerce/mpp-client.d.ts +11 -0
  24. package/dist/commerce/mpp-client.js +5 -0
  25. package/dist/commerce/mpp-runtime.d.ts +1 -0
  26. package/dist/commerce/mpp-runtime.js +1 -0
  27. package/dist/commerce/native-skills.d.ts +31 -0
  28. package/dist/commerce/native-skills.js +174 -0
  29. package/dist/commerce/operations.d.ts +22 -0
  30. package/dist/commerce/operations.js +39 -0
  31. package/dist/commerce/peer-proxy.js +11 -5
  32. package/dist/commerce/pricing.js +2 -2
  33. package/dist/commerce/runtime.d.ts +2 -0
  34. package/dist/commerce/runtime.js +2 -0
  35. package/dist/commerce/server.d.ts +4 -1
  36. package/dist/commerce/server.js +40 -9
  37. package/dist/commerce/setup.d.ts +18 -0
  38. package/dist/commerce/setup.js +344 -0
  39. package/dist/commerce/skill-gate.d.ts +54 -0
  40. package/dist/commerce/skill-gate.js +153 -0
  41. package/dist/commerce/store.d.ts +2 -0
  42. package/dist/commerce/store.js +7 -0
  43. package/dist/commerce/stripe-issuer.d.ts +41 -0
  44. package/dist/commerce/stripe-issuer.js +351 -0
  45. package/dist/commerce/stripe-provider.d.ts +2 -0
  46. package/dist/commerce/stripe-provider.js +57 -22
  47. package/dist/commerce/types.d.ts +27 -2
  48. package/dist/commerce/x402.d.ts +1 -0
  49. package/dist/commerce/x402.js +8 -1
  50. package/examples/hermes-setup-probe.py +105 -0
  51. package/examples/hermes-skill-inventory.py +107 -0
  52. package/examples/native-skill-runner.py +197 -0
  53. package/package.json +1 -1
  54. package/schemas/buyer-policy.schema.json +105 -0
  55. package/schemas/envarpay.config.schema.json +22 -15
@@ -1,5 +1,5 @@
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';
@@ -19,12 +19,19 @@ import { CredentialVault } from './vault.js';
19
19
  import { X402Gate } from './x402.js';
20
20
  import { evmReceiptVerifier, evmSettlementRecovery } from './chain.js';
21
21
  import { CommerceError } from './types.js';
22
+ import { createNativeSkillExecutor } from './native-skills.js';
23
+ import { inspectLedger, inspectLocalAlerts } from './operations.js';
24
+ import { setupHermes } from './setup.js';
25
+ import { SkillDiscovery } from './discovery.js';
26
+ import { upgradeDiscovery, configurePayments } from './configure.js';
22
27
  const { values, positionals } = parseArgs({ allowPositionals: true, options: {
23
28
  config: { type: 'string' }, service: { type: 'string' }, offer: { type: 'string' }, origin: { type: 'string' },
24
29
  version: { type: 'boolean' }, help: { type: 'boolean', short: 'h' }, state: { type: 'string' }, credentials: { type: 'string' }, host: { type: 'string' },
25
30
  port: { type: 'string' }, directory: { type: 'string' }, 'envar-config': { type: 'string' }, 'envar-credentials': { 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
+ file: { type: 'string' }, 'docker-container': { type: 'string' }, python: { type: 'string' }, 'hermes-home': { type: 'string' },
26
33
  } });
27
- const usage = 'envarpay init --directory DIR | 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 upgrade --directory DIR [--hermes-home PATH] | payments --directory DIR --file receiving.json\nenvarpay setup --file envar-setup.json [--docker-container NAME | --python PATH] [--hermes-home PATH] [--directory DIR]\ninit --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';
28
35
  const closers = [];
29
36
  function privateFile(path) {
30
37
  const stat = lstatSync(path);
@@ -84,6 +91,35 @@ async function run() {
84
91
  return;
85
92
  }
86
93
  const command = positionals[0];
94
+ if (command === 'upgrade' && values.directory) {
95
+ upgradeDiscovery(values.directory, values['hermes-home']);
96
+ return;
97
+ }
98
+ if (command === 'payments' && values.directory && values.file) {
99
+ await configurePayments(values.directory, values.file);
100
+ return;
101
+ }
102
+ if (command === 'setup') {
103
+ if (!values.file)
104
+ throw new CommerceError('setup_file', 'Download the setup file from your Agent service page and pass --file PATH');
105
+ await setupHermes({ file: values.file, directory: values.directory, container: values['docker-container'], python: values.python, home: values['hermes-home'] });
106
+ return;
107
+ }
108
+ if (values.alerts || values['fail-on-alert'] || values['integration-state'] || values['no-integration']) {
109
+ if (command !== 'inspect' || !values.state || !values.alerts || values['refund-review'] || values.after || values.limit || (values['no-integration'] && values['integration-state']))
110
+ throw new CommerceError('inspection_view', 'Alert inspection needs inspect --state and --alerts, without pagination or refund review; choose at most one integration source');
111
+ const report = inspectLocalAlerts(values.state, { policyPath: values.config, integrationPath: values['integration-state'], integrationDisabled: values['no-integration'] });
112
+ console.log(JSON.stringify(report, null, 2));
113
+ if (values['fail-on-alert'])
114
+ process.exitCode = report.recommendedExitCode;
115
+ return;
116
+ }
117
+ if (command === 'inspect' && values.state) {
118
+ console.log(JSON.stringify(inspectLedger(values.state, Number(values.after ?? 0), Number(values.limit ?? 100), values['refund-review'] ? 'refund-review' : 'attention'), null, 2));
119
+ return;
120
+ }
121
+ if (values['refund-review'])
122
+ throw new CommerceError('inspection_view', 'Refund review is available only for inspect');
87
123
  if ((values['envar-config'] || values['envar-credentials']) && command !== 'serve')
88
124
  throw new CommerceError('envar_role', 'Configuration synchronization is available on seller serve only');
89
125
  if (Boolean(values['envar-config']) !== Boolean(values['envar-credentials']))
@@ -117,15 +153,36 @@ async function run() {
117
153
  throw new CommerceError('buyer_key', 'Signer file must contain one 32-byte 0x-prefixed private key');
118
154
  signer = privateKeyToAccount(key);
119
155
  }
120
- if (needsMpp && !secrets.mppAdapterModule)
121
- throw new CommerceError('mpp_adapter_module', 'MPP peers require an explicit local adapter module for authorized funding and receipt verification');
122
- const mpp = needsMpp ? await loadMppAdapter(secrets.mppAdapterModule) : undefined;
156
+ if (secrets.mppAdapterModule && secrets.mppStripe)
157
+ throw new CommerceError('mpp_configuration', 'Choose the built-in Stripe issuer or one explicit adapter module');
158
+ if (needsMpp && !secrets.mppAdapterModule && !secrets.mppStripe)
159
+ throw new CommerceError('mpp_configuration', 'MPP peers require a configured Stripe issuer or explicit authorized funding adapter');
160
+ const buyerVault = secrets.vaultKeyFile ? new CredentialVault(join(dirname(values.state), 'buyer-authorizations'), privateFile(secrets.vaultKeyFile)) : undefined;
161
+ let mpp;
162
+ if (needsMpp && secrets.mppStripe) {
163
+ const configured = secrets.mppStripe;
164
+ if (!configured.secretKeyFile || !configured.sellers || typeof configured.sellers !== 'object' || Array.isArray(configured.sellers))
165
+ throw new CommerceError('mpp_configuration', 'Stripe issuer and seller read credentials must use private key files');
166
+ const { createStripeMppBuyer } = await import('./stripe-issuer.js');
167
+ const adapter = createStripeMppBuyer({
168
+ payer: configured.payer, mode: configured.mode, issuerAccountId: configured.issuerAccountId,
169
+ secretKey: privateFile(configured.secretKeyFile).toString('utf8').trim(), paymentMethod: configured.paymentMethod,
170
+ ...(configured.publishableKey !== undefined ? { publishableKey: configured.publishableKey } : {}),
171
+ sellers: Object.fromEntries(Object.entries(configured.sellers).map(([id, seller]) => [id, { accountId: seller.accountId, secretKey: privateFile(seller.secretKeyFile).toString('utf8').trim() }])),
172
+ ...(configured.issuance ? { issuance: configured.issuance } : {}), ...(configured.returnUrl ? { returnUrl: configured.returnUrl } : {}),
173
+ }, { statePath: values.state + '.stripe-issuer.sqlite3', vault: buyerVault });
174
+ closers.push(() => adapter.close());
175
+ await adapter.assertReady();
176
+ mpp = adapter;
177
+ }
178
+ else if (needsMpp)
179
+ mpp = await loadMppAdapter(secrets.mppAdapterModule);
123
180
  const { BuyerStore } = await import('./buyer-store.js');
124
181
  const { CommerceBuyer } = await import('./buyer.js');
125
182
  const { BuyerManagement, listenBuyerManagement } = await import('./management.js');
126
183
  const store = new BuyerStore(values.state);
127
184
  closers.push(() => store.close());
128
- const buyer = new CommerceBuyer({ policy, store, vault: secrets.vaultKeyFile ? new CredentialVault(join(dirname(values.state), 'buyer-authorizations'), privateFile(secrets.vaultKeyFile)) : undefined,
185
+ const buyer = new CommerceBuyer({ policy, store, vault: buyerVault,
129
186
  signer, mpp, peerTokens: secrets.peerTokens, peerEndpoints: secrets.peerEndpoints,
130
187
  ...(needsEvm ? { verifyReceipt: evmReceiptVerifier(secrets.rpcUrls), ...evmSettlementRecovery(secrets.rpcUrls) } : {}) });
131
188
  closers.push(() => buyer.stop());
@@ -171,6 +228,10 @@ async function run() {
171
228
  syncSettings = JSON.parse(privateFile(values['envar-config']).toString('utf8'));
172
229
  if (!syncSettings?.policy)
173
230
  throw new CommerceError('envar_configuration', 'Envar settings must include an explicit local policy');
231
+ if (secrets.skills?.discovery?.enabledFromWeb) {
232
+ 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);
233
+ syncSettings.policy.allowedServices = [...new Set([...syncSettings.policy.allowedServices, ...installed])];
234
+ }
174
235
  const tokenFile = resolve(values['envar-credentials']);
175
236
  privateFile(tokenFile);
176
237
  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() });
@@ -201,7 +262,7 @@ async function run() {
201
262
  if (!secrets.payers || !secrets.rpcUrls)
202
263
  throw new CommerceError('x402_configuration', 'x402 requires explicit caller-payer bindings and network RPCs');
203
264
  const gate = new X402Gate({ store, vault: vault, payerFor: caller => secrets.payers?.[caller], verifyReceipt: evmReceiptVerifier(secrets.rpcUrls), ...evmSettlementRecovery(secrets.rpcUrls) });
204
- await gate.check(Object.values(config.paymentProfiles).filter((p) => p.adapter === 'x402'));
265
+ await gate.check([...Object.values(config.paymentProfiles).filter((p) => p.adapter === 'x402'), ...(secrets.receivingProfiles ?? [])]);
205
266
  gates.x402 = gate;
206
267
  }
207
268
  if (protocols.has('mpp')) {
@@ -217,7 +278,11 @@ async function run() {
217
278
  gates.mpp = gate;
218
279
  stripeRecipientFor = ref => gate.merchantRecipient(ref);
219
280
  }
220
- const upstreams = new PinnedUpstreams(join(dirname(values.state), 'upstream-bindings.json'), secrets.upstreams, secrets.upstreamInputEncoding, fetch, controller.signal);
281
+ const nativeSkills = secrets.skills ? createNativeSkillExecutor(secrets.skills, values.origin, config.agent.name) : undefined;
282
+ const discovery = secrets.skills?.discovery?.enabledFromWeb ? new SkillDiscovery(secrets.skills, secrets.skills.discovery, values.origin) : undefined;
283
+ if (nativeSkills)
284
+ closers.push(() => nativeSkills.close());
285
+ const upstreams = new PinnedUpstreams(join(dirname(values.state), 'upstream-bindings.json'), secrets.upstreams ?? {}, secrets.upstreamInputEncoding, fetch, controller.signal, nativeSkills?.execute);
221
286
  upstreams.prepare(config, store.catalogHistory().map(entry => entry.service));
222
287
  const validateAdapters = (candidate) => {
223
288
  for (const profile of Object.values(candidate.paymentProfiles)) {
@@ -242,7 +307,7 @@ async function run() {
242
307
  await new Promise(r => setTimeout(r, 50)); });
243
308
  let connection;
244
309
  if (integration) {
245
- connection = new EnvarSellerRuntime({ integration, server, upstreams, pollIntervalMs: syncSettings?.pollIntervalMs, validateConfig: validateAdapters, onError: code => console.error(JSON.stringify({ code, integration: 'envar' })) });
310
+ connection = new EnvarSellerRuntime({ integration, server, upstreams, pollIntervalMs: syncSettings?.pollIntervalMs, validateConfig: validateAdapters, beforeSync: discovery ? () => discovery.sync(integration, [...Object.values(server.config.paymentProfiles).filter(p => p.adapter === 'x402'), ...(secrets.receivingProfiles ?? [])].map(p => ({ network: p.network, pay_to: p.payTo, facilitator_url: p.facilitatorUrl }))) : undefined, onError: code => console.error(JSON.stringify({ code, integration: 'envar' })) });
246
311
  closers.push(() => connection.stop());
247
312
  }
248
313
  const http = listenCommerce(server, values.origin, values.host ?? '127.0.0.1', port(4020));
@@ -4,5 +4,5 @@ export * from './buyer.js';
4
4
  export * from './management.js';
5
5
  export * from './vault.js';
6
6
  export * from './chain.js';
7
- export type { MppBuyerOptions, MppTokenOperation, MppVerificationContext, MppReceipt } from './mpp-client.js';
7
+ export type { MppAuthenticationAction, MppBuyerOptions, MppTokenOperation, MppVerificationContext, MppReceipt } from './mpp-client.js';
8
8
  export * from './peer-proxy.js';
@@ -1,7 +1,11 @@
1
- import { type BuyerPolicy, type CommerceConfig, type PaymentProfile } from './types.js';
1
+ import { type BuyerPolicy, 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;
5
7
  export declare function digest(value: unknown): string;
8
+ /** Keep legacy quotes unchanged; skill-bound quotes freeze the installed skill revision too. */
9
+ export declare function serviceTermsDigest(configVersion: 1 | 2, service: Service, offer: Offer, profile: PaymentProfile | null): string;
6
10
  export declare function loadCommerceConfig(value: unknown): CommerceConfig;
7
11
  export declare function loadBuyerPolicy(value: unknown): BuyerPolicy;
@@ -45,6 +45,10 @@ export function validateUrl(value, allowPrivate = false) {
45
45
  export function currencyOf(profile) {
46
46
  return profile.adapter === 'x402' ? `${profile.network}/erc20:${profile.asset.toLowerCase()}` : profile.currency;
47
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);
51
+ }
48
52
  function assertJson(value) {
49
53
  if (typeof value === 'number' && (!Number.isFinite(value) || (Number.isInteger(value) && !Number.isSafeInteger(value))))
50
54
  throw new CommerceError('unsafe_json_number', 'Use strings for integers outside the safe JSON number range');
@@ -64,6 +68,13 @@ export function digest(value) {
64
68
  throw new CommerceError('invalid_json', 'Cannot canonicalize input');
65
69
  return createHash('sha256').update(serialized).digest('hex');
66
70
  }
71
+ /** Keep legacy quotes unchanged; skill-bound quotes freeze the installed skill revision too. */
72
+ export function serviceTermsDigest(configVersion, service, offer, profile) {
73
+ const terms = { contract: service.contract, offer, profile };
74
+ return digest(configVersion === 2 && service.execution.type === 'skill'
75
+ ? { ...terms, skill: { name: service.id, digest: service.execution.skillDigest } }
76
+ : terms);
77
+ }
67
78
  export function loadCommerceConfig(value) {
68
79
  if (!sellerValidator(value))
69
80
  throw new CommerceError('invalid_config', ajv.errorsText(sellerValidator.errors));
@@ -80,6 +91,13 @@ export function loadCommerceConfig(value) {
80
91
  if (services.has(service.id))
81
92
  throw new CommerceError('duplicate_service', 'Service IDs must be unique');
82
93
  services.add(service.id);
94
+ if (config.configVersion === 2 && service.execution.type === 'skill') {
95
+ if (service.id !== service.name)
96
+ throw new CommerceError('skill_name_mismatch', 'A version 2 service must use the exact installed skill name as its ID and name');
97
+ }
98
+ else if (config.configVersion === 1 && service.execution.type !== 'a2a') {
99
+ throw new CommerceError('legacy_skill_mode', 'Skill-bound services require configVersion 2');
100
+ }
83
101
  validateUrl(service.execution.cardUrl, true);
84
102
  noExternalRefs(service.contract.inputSchema);
85
103
  const inputAjv = new Ajv2020({ strict: true });
@@ -143,6 +161,24 @@ export function loadBuyerPolicy(value) {
143
161
  const ids = new Set();
144
162
  for (const peer of policy.peers) {
145
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
+ }
146
182
  if (ids.has(peer.id))
147
183
  throw new CommerceError('duplicate_peer', 'Peer IDs must be unique');
148
184
  ids.add(peer.id);
@@ -154,7 +190,7 @@ export function loadBuyerPolicy(value) {
154
190
  throw new CommerceError('invalid_recipient', 'x402 peers require a nonzero EVM recipient and USDC asset');
155
191
  peer.recipient = peer.recipient.toLowerCase();
156
192
  }
157
- else if (peer.currency !== 'usd' || !/^profile_[A-Za-z0-9]+$/.test(peer.recipient))
193
+ else if (peer.currency !== 'usd' || !isStripeProfileId(peer.recipient))
158
194
  throw new CommerceError('invalid_recipient', 'MPP peers require USD and a verified Stripe merchant profile');
159
195
  const limit = limits.get(peer.currency);
160
196
  if (limit === undefined || atomic(peer.maxPerPurchase) > limit)
@@ -0,0 +1,3 @@
1
+ export declare function refreshLauncher(directory: string): void;
2
+ export declare function upgradeDiscovery(directory: string, home?: string): void;
3
+ export declare function configurePayments(directory: string, file: string): Promise<void>;
@@ -0,0 +1,158 @@
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 { randomBytes } from "node:crypto";
5
+ import { createInterface } from "node:readline/promises";
6
+ import { CommerceError } from "./types.js";
7
+ import { validateUrl, loadCommerceConfig } from "./config.js";
8
+ function read(path) {
9
+ const s = lstatSync(path);
10
+ if (!s.isFile() ||
11
+ s.isSymbolicLink() ||
12
+ s.size > 1024 * 1024 ||
13
+ s.mode & 0o077)
14
+ throw new CommerceError("private_file", "Use an owner-only configuration file");
15
+ return JSON.parse(readFileSync(path, "utf8"));
16
+ }
17
+ function save(path, value) {
18
+ const temp = path + ".tmp";
19
+ writeFileSync(temp, JSON.stringify(value, null, 2) + "\n", {
20
+ mode: 0o600,
21
+ flag: "wx",
22
+ });
23
+ renameSync(temp, path);
24
+ }
25
+ export function refreshLauncher(directory) {
26
+ const root = resolve(directory), connection = read(join(root, "connection.json")), settingsPath = join(root, "runtime.json");
27
+ // alpha.10 start scripts recorded the port in generated source. Parse JSON argv only; never evaluate code.
28
+ let port;
29
+ if (existsSync(settingsPath))
30
+ port = read(settingsPath).port;
31
+ else {
32
+ const old = readFileSync(join(root, "start.mjs"), "utf8");
33
+ const array = /spawn\([^\n]*?,(\[[^\n]+\]),\{stdio:/.exec(old)?.[1];
34
+ if (!array)
35
+ throw new CommerceError("setup_upgrade", "Cannot infer original start settings. Use serve with original files.");
36
+ const args = JSON.parse(array);
37
+ port = Number(args[args.indexOf("--port") + 1]);
38
+ }
39
+ if (!Number.isInteger(port) || port < 1024 || port > 65535)
40
+ throw new CommerceError("setup_port", "Invalid local service port");
41
+ const args = [
42
+ fileURLToPath(new URL("./cli.js", import.meta.url)),
43
+ "serve",
44
+ "--config",
45
+ join(root, "seller.json"),
46
+ "--credentials",
47
+ join(root, "credentials.json"),
48
+ "--state",
49
+ join(root, "seller.sqlite3"),
50
+ "--origin",
51
+ new URL(connection.url).origin,
52
+ "--host",
53
+ "127.0.0.1",
54
+ "--port",
55
+ String(port),
56
+ "--envar-config",
57
+ join(root, "envar.json"),
58
+ "--envar-credentials",
59
+ join(root, "envar.token"),
60
+ ];
61
+ save(settingsPath, { port });
62
+ const start = join(root, "start.mjs");
63
+ 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 });
64
+ }
65
+ export function upgradeDiscovery(directory, home) {
66
+ const root = resolve(directory), file = join(root, "credentials.json"), credentials = read(file);
67
+ if (credentials.skills?.framework !== "hermes")
68
+ throw new CommerceError("setup_framework", "This discovery setup supports Hermes");
69
+ const skill = credentials.skills;
70
+ const hermesHome = home ??
71
+ skill.discovery?.hermesHome ??
72
+ (skill.dockerStateDirectory
73
+ ? dirname(dirname(skill.dockerStateDirectory))
74
+ : undefined);
75
+ if (!hermesHome)
76
+ throw new CommerceError("setup_home", "Pass --hermes-home with your existing Hermes profile directory");
77
+ credentials.skills.discovery = { hermesHome, enabledFromWeb: true };
78
+ refreshLauncher(root);
79
+ save(file, credentials);
80
+ 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.`);
81
+ }
82
+ export async function configurePayments(directory, file) {
83
+ if (!process.stdin.isTTY)
84
+ throw new CommerceError("setup_terminal", "Use an interactive terminal to approve receiving configuration");
85
+ const root = resolve(directory), bundle = JSON.parse(readFileSync(file, "utf8"));
86
+ const cfg = read(join(root, "seller.json")), auth = read(join(root, "credentials.json")), sync = read(join(root, "envar.json"));
87
+ const assets = {
88
+ "eip155:8453": "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913",
89
+ "eip155:84532": "0x036cbd53842c5426634e7929541ec2318f3dcf7e",
90
+ };
91
+ if (bundle.version !== 1 ||
92
+ bundle.agentId !== sync.policy.agentId ||
93
+ !assets[bundle.network] ||
94
+ !/^0x[0-9a-fA-F]{40}$/.test(bundle.payTo) ||
95
+ BigInt(bundle.payTo) === 0n)
96
+ throw new CommerceError("payment_setup", "Download receiving settings for this Agent");
97
+ const prompt = createInterface({
98
+ input: process.stdin,
99
+ output: process.stdout,
100
+ });
101
+ const ask = async (text, fallback = "") => (await prompt.question(`${text}${fallback ? ` [${fallback}]` : ""}: `)).trim() || fallback;
102
+ try {
103
+ console.log(`Receiving account: ${bundle.payTo}\nNetwork: ${bundle.network === "eip155:8453" ? "Base mainnet" : "Base Sepolia testnet"}\nAsset: official USDC. This enables receiving only; it does not send money or create a signing key.`);
104
+ if ((await ask("Approve this receiving account? (yes/no)", "no")) !== "yes")
105
+ return;
106
+ const facilitator = validateUrl(await ask("x402 facilitator HTTPS URL", bundle.facilitatorUrl ?? ""));
107
+ const rpc = validateUrl(await ask("Network RPC HTTPS URL", bundle.network === "eip155:8453"
108
+ ? "https://mainnet.base.org"
109
+ : "https://sepolia.base.org"));
110
+ const payer = await ask("Authorized buyer wallet address");
111
+ if (!/^0x[0-9a-fA-F]{40}$/.test(payer) || BigInt(payer) === 0n)
112
+ throw new CommerceError("payment_setup", "Use the buyer wallet address for this service access credential");
113
+ const caller = "buyer-" + payer.slice(2).toLowerCase(), token = randomBytes(36).toString("base64url");
114
+ auth.callers = { ...auth.callers, [token]: caller };
115
+ auth.payers = { ...auth.payers, [caller]: payer };
116
+ auth.rpcUrls = { ...auth.rpcUrls, [bundle.network]: rpc.href };
117
+ auth.vaultKeyFile ??= join(root, "vault.key");
118
+ if (!existsSync(auth.vaultKeyFile))
119
+ writeFileSync(auth.vaultKeyFile, randomBytes(32), {
120
+ mode: 0o600,
121
+ flag: "wx",
122
+ });
123
+ const profile = {
124
+ adapter: "x402",
125
+ scheme: "exact",
126
+ network: bundle.network,
127
+ asset: assets[bundle.network],
128
+ payTo: bundle.payTo,
129
+ facilitatorUrl: facilitator.href,
130
+ };
131
+ loadCommerceConfig({
132
+ ...cfg,
133
+ paymentProfiles: { ...cfg.paymentProfiles, "setup-receiving": profile },
134
+ });
135
+ auth.receivingProfiles = [
136
+ ...(auth.receivingProfiles ?? []).filter((p) => p.network !== bundle.network),
137
+ profile,
138
+ ];
139
+ sync.policy.allowedX402 = [
140
+ ...sync.policy.allowedX402.filter((p) => p.network !== bundle.network),
141
+ {
142
+ network: bundle.network,
143
+ asset: assets[bundle.network],
144
+ payTo: bundle.payTo.toLowerCase(),
145
+ facilitatorOrigin: facilitator.origin,
146
+ },
147
+ ];
148
+ const accessFile = join(root, caller + ".token");
149
+ writeFileSync(accessFile, token + "\n", { mode: 0o600 });
150
+ save(join(root, "credentials.json"), auth);
151
+ save(join(root, "envar.json"), sync);
152
+ refreshLauncher(root);
153
+ console.log(`Receiving configured for ${cfg.agent.name}. Restart the local service using start.mjs, then refresh Envar.\nBuyer service-access token: ${accessFile}\nGive this file only to the authorized buyer. Original wallets, budgets and ledgers were preserved. No payment performed.`);
154
+ }
155
+ finally {
156
+ prompt.close();
157
+ }
158
+ }
@@ -0,0 +1,23 @@
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
+ }
7
+ export declare class SkillDiscovery {
8
+ private settings;
9
+ private discovery;
10
+ private origin;
11
+ private enabled;
12
+ private cached;
13
+ private nextScan;
14
+ private state;
15
+ readonly path: string;
16
+ constructor(settings: NativeSkillSettings, discovery: DiscoverySettings, origin: string);
17
+ private scan;
18
+ sync(integration: EnvarIntegration, payments: {
19
+ network: string;
20
+ pay_to: string;
21
+ facilitator_url: string;
22
+ }[]): Promise<void>;
23
+ }
@@ -0,0 +1,162 @@
1
+ import { execFile } from "node:child_process";
2
+ import { promisify } from "node:util";
3
+ import { readFileSync, writeFileSync, existsSync, mkdtempSync, mkdirSync, renameSync, rmSync, readdirSync, lstatSync, } from "node:fs";
4
+ import { createHash } from 'node:crypto';
5
+ import { join } from "node:path";
6
+ import { createRequire } from "node:module";
7
+ import { prepareTextSkill } from "./setup.js";
8
+ import { readInstalledSkills } from "./native-skills.js";
9
+ const exec = promisify(execFile);
10
+ function packageDigest(root) {
11
+ const files = [];
12
+ let size = 0;
13
+ function walk(folder, prefix = '') {
14
+ if (prefix.split('/').length > 12)
15
+ throw Error('skill_size');
16
+ for (const name of readdirSync(folder).sort()) {
17
+ const path = join(folder, name), stat = lstatSync(path);
18
+ if (stat.isSymbolicLink())
19
+ throw Error('skill_symlink');
20
+ if (stat.isDirectory()) {
21
+ walk(path, prefix + name + '/');
22
+ continue;
23
+ }
24
+ if (!stat.isFile() || stat.size > 512 * 1024 || (size += stat.size) > 1024 * 1024 || files.length >= 128)
25
+ throw Error('skill_size');
26
+ files.push([prefix + name, createHash('sha256').update(readFileSync(path)).digest('hex')]);
27
+ }
28
+ }
29
+ walk(root);
30
+ files.sort((a, b) => a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0);
31
+ const json = JSON.stringify(files).replace(/[\u007f-\uffff]/g, c => '\\u' + c.charCodeAt(0).toString(16).padStart(4, '0'));
32
+ return createHash('sha256').update(json).digest('hex');
33
+ }
34
+ export class SkillDiscovery {
35
+ settings;
36
+ discovery;
37
+ origin;
38
+ enabled;
39
+ cached = [];
40
+ nextScan = 0;
41
+ state = "ready";
42
+ path;
43
+ constructor(settings, discovery, origin) {
44
+ this.settings = settings;
45
+ this.discovery = discovery;
46
+ this.origin = origin;
47
+ this.path = join(settings.skillsDirectory, ".source-digests.json");
48
+ this.enabled = existsSync(this.path)
49
+ ? JSON.parse(readFileSync(this.path, "utf8"))
50
+ : {};
51
+ }
52
+ async scan() {
53
+ const code = readFileSync(new URL("../../examples/hermes-skill-inventory.py", import.meta.url), "utf8");
54
+ const args = ["-c", code, this.discovery.hermesHome];
55
+ const { stdout } = this.settings.dockerContainer
56
+ ? await exec("docker", [
57
+ "exec",
58
+ this.settings.dockerContainer,
59
+ this.settings.python,
60
+ ...args,
61
+ ], { timeout: 30000, maxBuffer: 1024 * 1024 })
62
+ : await exec(this.settings.python, args, {
63
+ timeout: 30000,
64
+ maxBuffer: 1024 * 1024,
65
+ });
66
+ const list = JSON.parse(stdout);
67
+ if (!Array.isArray(list) ||
68
+ list.length > 128 ||
69
+ list.some((s) => !/^[a-z][a-z0-9-]{0,63}$/.test(s.name) ||
70
+ !/^[0-9a-f]{64}$/.test(s.digest) ||
71
+ typeof s.path !== "string"))
72
+ throw Error("inventory");
73
+ return list;
74
+ }
75
+ async sync(integration, payments) {
76
+ if (Date.now() >= this.nextScan) {
77
+ this.nextScan = Date.now() + 15000;
78
+ try {
79
+ this.cached = await this.scan();
80
+ this.state = "ready";
81
+ }
82
+ catch {
83
+ this.state = this.settings.dockerContainer
84
+ ? "agent_unavailable"
85
+ : "scan_failed";
86
+ }
87
+ }
88
+ const active = new Set(readdirSync(this.settings.skillsDirectory, { withFileTypes: true })
89
+ .filter((x) => x.isDirectory() && !x.name.startsWith("."))
90
+ .map((x) => x.name));
91
+ for (const skill of this.cached)
92
+ if (active.has(skill.name) && !this.enabled[skill.name])
93
+ this.enabled[skill.name] = skill.digest;
94
+ if (this.state === "ready")
95
+ writeFileSync(this.path, JSON.stringify(this.enabled), { mode: 0o600 });
96
+ const skills = this.cached.map((s) => ({
97
+ name: s.name,
98
+ digest: s.digest,
99
+ description: s.description,
100
+ status: !s.supported
101
+ ? "requires_adapter"
102
+ : active.has(s.name)
103
+ ? this.enabled[s.name] && this.enabled[s.name] !== s.digest
104
+ ? "updated"
105
+ : "enabled"
106
+ : "available",
107
+ }));
108
+ for (const name of active)
109
+ if (!skills.some((s) => s.name === name))
110
+ skills.push({
111
+ name,
112
+ digest: this.enabled[name] ?? "0".repeat(64),
113
+ description: "",
114
+ status: "removed",
115
+ });
116
+ const result = await integration.reportRuntime({
117
+ version: createRequire(import.meta.url)("../../package.json").version,
118
+ card_url: this.origin + "/.well-known/agent-card.json",
119
+ state: this.state,
120
+ skills: skills.slice(0, 128),
121
+ payments: payments
122
+ .filter((p, i, all) => all.findIndex((x) => JSON.stringify(x) === JSON.stringify(p)) === i)
123
+ .slice(0, 8),
124
+ });
125
+ for (const request of result) {
126
+ const skill = this.cached.find((s) => s.name === request.name && s.digest === request.digest && s.supported);
127
+ if (!skill || active.has(skill.name) || this.state !== "ready")
128
+ continue;
129
+ // Check source again after the owner's request. Never overwrite a live package.
130
+ const current = (await this.scan()).find((s) => s.name === skill.name && s.digest === skill.digest && s.supported);
131
+ if (!current)
132
+ continue;
133
+ const stage = mkdtempSync(join(this.settings.skillsDirectory, ".prepare-"));
134
+ try {
135
+ let source = skill.path;
136
+ if (this.settings.dockerContainer) {
137
+ source = join(stage, "source");
138
+ await exec("docker", ["cp", `${this.settings.dockerContainer}:${skill.path}`, source], { timeout: 30000, maxBuffer: 1024 * 1024 });
139
+ }
140
+ if (packageDigest(source) !== skill.digest)
141
+ continue;
142
+ const prepared = join(stage, "prepared");
143
+ mkdirSync(prepared);
144
+ prepareTextSkill(source, join(prepared, skill.name), skill.name);
145
+ readInstalledSkills(prepared, [], this.origin + "/.well-known/agent-card.json");
146
+ const after = (await this.scan()).find((s) => s.name === skill.name);
147
+ if (after?.digest !== skill.digest)
148
+ continue;
149
+ renameSync(join(prepared, skill.name), join(this.settings.skillsDirectory, skill.name));
150
+ this.enabled[skill.name] = skill.digest;
151
+ writeFileSync(this.path, JSON.stringify(this.enabled), { mode: 0o600 });
152
+ // This grant includes only the exact installed package explicitly selected in the owner UI.
153
+ integration.allowInstalledService(skill.name);
154
+ }
155
+ finally {
156
+ rmSync(stage, { recursive: true, force: true });
157
+ }
158
+ }
159
+ for (const name of active)
160
+ integration.allowInstalledService(name);
161
+ }
162
+ }
@@ -8,10 +8,11 @@ export declare class PinnedUpstreams {
8
8
  private readonly inputEncoding;
9
9
  private readonly fetchImpl;
10
10
  private readonly signal?;
11
+ private readonly skills?;
11
12
  private bindings;
12
13
  private readonly executors;
13
14
  readonly execute: ExecuteOrder;
14
- constructor(path: string, credentials: Readonly<Record<string, string>>, inputEncoding?: Readonly<Record<string, "data" | "json-text">>, fetchImpl?: typeof fetch, signal?: AbortSignal | undefined);
15
+ constructor(path: string, credentials: Readonly<Record<string, string>>, inputEncoding?: Readonly<Record<string, "data" | "json-text">>, fetchImpl?: typeof fetch, signal?: AbortSignal | undefined, skills?: ExecuteOrder | undefined);
15
16
  prepare(config: CommerceConfig, history?: readonly Service[]): void;
16
17
  private executor;
17
18
  }
@@ -30,6 +31,7 @@ export interface EnvarSellerRuntimeOptions {
30
31
  /** Local payment adapters/merchant credentials must already exist; config sync cannot create them. */
31
32
  validateConfig?: (config: CommerceConfig) => void;
32
33
  onError?: (code: string) => void;
34
+ beforeSync?: () => Promise<void>;
33
35
  }
34
36
  export declare class EnvarSellerRuntime {
35
37
  private readonly options;