@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.
- package/README.md +2 -0
- package/dist/commerce/alerts.d.ts +37 -0
- package/dist/commerce/alerts.js +127 -0
- package/dist/commerce/buyer-store.d.ts +8 -0
- package/dist/commerce/buyer.d.ts +12 -3
- package/dist/commerce/buyer.js +151 -7
- package/dist/commerce/card.js +2 -2
- package/dist/commerce/cli.js +74 -9
- package/dist/commerce/client-runtime.d.ts +1 -1
- package/dist/commerce/config.d.ts +5 -1
- package/dist/commerce/config.js +37 -1
- package/dist/commerce/configure.d.ts +3 -0
- package/dist/commerce/configure.js +158 -0
- package/dist/commerce/discovery.d.ts +23 -0
- package/dist/commerce/discovery.js +162 -0
- package/dist/commerce/envar-runtime.d.ts +3 -1
- package/dist/commerce/envar-runtime.js +28 -1
- package/dist/commerce/envar.d.ts +5 -0
- package/dist/commerce/envar.js +13 -0
- package/dist/commerce/index.d.ts +1 -0
- package/dist/commerce/index.js +1 -0
- package/dist/commerce/management.js +3 -1
- package/dist/commerce/mpp-client.d.ts +11 -0
- package/dist/commerce/mpp-client.js +5 -0
- package/dist/commerce/mpp-runtime.d.ts +1 -0
- package/dist/commerce/mpp-runtime.js +1 -0
- package/dist/commerce/native-skills.d.ts +31 -0
- package/dist/commerce/native-skills.js +174 -0
- package/dist/commerce/operations.d.ts +22 -0
- package/dist/commerce/operations.js +39 -0
- package/dist/commerce/peer-proxy.js +11 -5
- package/dist/commerce/pricing.js +2 -2
- package/dist/commerce/runtime.d.ts +2 -0
- package/dist/commerce/runtime.js +2 -0
- package/dist/commerce/server.d.ts +4 -1
- package/dist/commerce/server.js +40 -9
- package/dist/commerce/setup.d.ts +18 -0
- package/dist/commerce/setup.js +344 -0
- package/dist/commerce/skill-gate.d.ts +54 -0
- package/dist/commerce/skill-gate.js +153 -0
- package/dist/commerce/store.d.ts +2 -0
- package/dist/commerce/store.js +7 -0
- package/dist/commerce/stripe-issuer.d.ts +41 -0
- package/dist/commerce/stripe-issuer.js +351 -0
- package/dist/commerce/stripe-provider.d.ts +2 -0
- package/dist/commerce/stripe-provider.js +57 -22
- package/dist/commerce/types.d.ts +27 -2
- package/dist/commerce/x402.d.ts +1 -0
- package/dist/commerce/x402.js +8 -1
- package/examples/hermes-setup-probe.py +105 -0
- package/examples/hermes-skill-inventory.py +107 -0
- package/examples/native-skill-runner.py +197 -0
- package/package.json +1 -1
- package/schemas/buyer-policy.schema.json +105 -0
- package/schemas/envarpay.config.schema.json +22 -15
package/dist/commerce/cli.js
CHANGED
|
@@ -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
|
|
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 (
|
|
121
|
-
throw new CommerceError('
|
|
122
|
-
|
|
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:
|
|
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
|
|
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;
|
package/dist/commerce/config.js
CHANGED
|
@@ -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' ||
|
|
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,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;
|