@envarai/envarpay 0.2.0-alpha.19 → 0.2.0-alpha.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/README.md +18 -63
  2. package/dist/commerce/alerts.d.ts +5 -15
  3. package/dist/commerce/alerts.js +21 -77
  4. package/dist/commerce/card.js +1 -1
  5. package/dist/commerce/cli.js +17 -145
  6. package/dist/commerce/config.d.ts +1 -4
  7. package/dist/commerce/config.js +4 -82
  8. package/dist/commerce/configure.d.ts +0 -1
  9. package/dist/commerce/configure.js +0 -80
  10. package/dist/commerce/discovery.d.ts +1 -5
  11. package/dist/commerce/discovery.js +1 -4
  12. package/dist/commerce/envar-balance.js +1 -1
  13. package/dist/commerce/envar.d.ts +1 -32
  14. package/dist/commerce/envar.js +3 -82
  15. package/dist/commerce/operations.d.ts +1 -2
  16. package/dist/commerce/operations.js +17 -20
  17. package/dist/commerce/pricing.d.ts +0 -1
  18. package/dist/commerce/pricing.js +1 -3
  19. package/dist/commerce/runtime.d.ts +0 -2
  20. package/dist/commerce/runtime.js +0 -2
  21. package/dist/commerce/server.d.ts +0 -1
  22. package/dist/commerce/server.js +2 -3
  23. package/dist/commerce/setup.js +4 -3
  24. package/dist/commerce/types.d.ts +0 -59
  25. package/examples/envar-integration-policy.json +1 -9
  26. package/examples/seller.json +143 -26
  27. package/package.json +3 -16
  28. package/schemas/envarpay.config.schema.json +1 -80
  29. package/dist/commerce/buyer-store.d.ts +0 -101
  30. package/dist/commerce/buyer-store.js +0 -168
  31. package/dist/commerce/buyer.d.ts +0 -138
  32. package/dist/commerce/buyer.js +0 -775
  33. package/dist/commerce/chain.d.ts +0 -24
  34. package/dist/commerce/chain.js +0 -196
  35. package/dist/commerce/client-runtime.d.ts +0 -8
  36. package/dist/commerce/client-runtime.js +0 -7
  37. package/dist/commerce/management.d.ts +0 -14
  38. package/dist/commerce/management.js +0 -140
  39. package/dist/commerce/mpp-client.d.ts +0 -70
  40. package/dist/commerce/mpp-client.js +0 -29
  41. package/dist/commerce/mpp-runtime.d.ts +0 -4
  42. package/dist/commerce/mpp-runtime.js +0 -4
  43. package/dist/commerce/mpp.d.ts +0 -53
  44. package/dist/commerce/mpp.js +0 -163
  45. package/dist/commerce/peer-proxy.d.ts +0 -44
  46. package/dist/commerce/peer-proxy.js +0 -673
  47. package/dist/commerce/stripe-issuer.d.ts +0 -41
  48. package/dist/commerce/stripe-issuer.js +0 -351
  49. package/dist/commerce/stripe-provider.d.ts +0 -11
  50. package/dist/commerce/stripe-provider.js +0 -132
  51. package/dist/commerce/x402.d.ts +0 -38
  52. package/dist/commerce/x402.js +0 -181
  53. package/examples/buyer-policy.json +0 -18
  54. package/schemas/buyer-policy.schema.json +0 -253
@@ -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,75 +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');
138
- if (profile.adapter === 'envar_balance' && high > 2n ** 63n - 1n)
119
+ if (high > 2n ** 63n - 1n)
139
120
  throw new CommerceError('balance_amount', 'Envar balance prices must fit signed micro-USD');
140
121
  }
141
122
  }
142
123
  return config;
143
124
  }
144
- export function loadBuyerPolicy(value) {
145
- if (!buyerValidator(value))
146
- throw new CommerceError('invalid_policy', ajv.errorsText(buyerValidator.errors));
147
- const policy = structuredClone(value);
148
- const normalizedCurrency = (currency) => {
149
- if (currency === 'usd')
150
- return currency;
151
- const match = /^(eip155:[1-9][0-9]*)\/erc20:(0x[0-9a-fA-F]{40})$/.exec(currency);
152
- if (!match || USDC[match[1]] !== match[2].toLowerCase())
153
- throw new CommerceError('unsupported_currency', 'Buyer budget must use USD or an official supported USDC asset');
154
- return `${match[1]}/erc20:${match[2].toLowerCase()}`;
155
- };
156
- const limits = new Map();
157
- for (const b of policy.budgets) {
158
- b.currency = normalizedCurrency(b.currency);
159
- if (limits.has(b.currency))
160
- throw new CommerceError('duplicate_budget', 'Only one budget per currency is allowed');
161
- limits.set(b.currency, atomic(b.maxTotal));
162
- }
163
- const ids = new Set();
164
- for (const peer of policy.peers) {
165
- validateUrl(peer.cardUrl);
166
- if (peer.mode === 'standard-a2a') {
167
- const endpoint = validateUrl(peer.endpoint), card = validateUrl(peer.cardUrl);
168
- if (endpoint.origin !== card.origin || endpoint.search || endpoint.href !== peer.endpoint || card.href !== peer.cardUrl)
169
- throw new CommerceError('standard_peer_endpoint', 'Review an exact same-origin HTTPS A2A endpoint without query parameters');
170
- noExternalRefs(peer.localContract.inputSchema);
171
- if (JSON.stringify(peer.localContract.inputSchema).length > 512 * 1024)
172
- throw new CommerceError('standard_peer_schema', 'Local input schema exceeds 512 KiB');
173
- try {
174
- const inputAjv = new Ajv2020({ strict: true });
175
- addFormats(inputAjv);
176
- inputAjv.compile(peer.localContract.inputSchema);
177
- }
178
- catch {
179
- throw new CommerceError('standard_peer_schema', 'Local input schema must be a valid supported JSON Schema');
180
- }
181
- if (atomic(peer.localContract.amount) > atomic(peer.maxPerPurchase))
182
- throw new CommerceError('purchase_limit', 'Locally reviewed standard-peer price exceeds the per-purchase limit');
183
- }
184
- if (ids.has(peer.id))
185
- throw new CommerceError('duplicate_peer', 'Peer IDs must be unique');
186
- ids.add(peer.id);
187
- if (peer.protocol === 'free')
188
- continue;
189
- peer.currency = normalizedCurrency(peer.currency);
190
- if (peer.protocol === 'x402') {
191
- if (!/^0x[0-9a-fA-F]{40}$/.test(peer.recipient) || BigInt(peer.recipient) === 0n || peer.currency === 'usd')
192
- throw new CommerceError('invalid_recipient', 'x402 peers require a nonzero EVM recipient and USDC asset');
193
- peer.recipient = peer.recipient.toLowerCase();
194
- }
195
- else if (peer.currency !== 'usd' || !isStripeProfileId(peer.recipient))
196
- throw new CommerceError('invalid_recipient', 'MPP peers require USD and a verified Stripe merchant profile');
197
- const limit = limits.get(peer.currency);
198
- if (limit === undefined || atomic(peer.maxPerPurchase) > limit)
199
- throw new CommerceError('invalid_budget', 'Each peer needs a cumulative budget at least as large as its per-purchase limit');
200
- }
201
- return policy;
202
- }
@@ -1,3 +1,2 @@
1
1
  export declare function refreshLauncher(directory: string): void;
2
2
  export declare function upgradeDiscovery(directory: string, home?: string): void;
3
- export declare function configurePayments(directory: string, file: string): Promise<void>;
@@ -1,10 +1,7 @@
1
1
  import { readFileSync, writeFileSync, lstatSync, existsSync, renameSync, } from "node:fs";
2
2
  import { join, resolve, dirname } from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
- import { randomBytes } from "node:crypto";
5
- import { createInterface } from "node:readline/promises";
6
4
  import { CommerceError } from "./types.js";
7
- import { validateUrl, loadCommerceConfig } from "./config.js";
8
5
  function read(path) {
9
6
  const s = lstatSync(path);
10
7
  if (!s.isFile() ||
@@ -79,80 +76,3 @@ export function upgradeDiscovery(directory, home) {
79
76
  save(file, credentials);
80
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.`);
81
78
  }
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
- }
@@ -16,9 +16,5 @@ export declare class SkillDiscovery {
16
16
  readonly path: string;
17
17
  constructor(settings: NativeSkillSettings, discovery: DiscoverySettings, origin: string);
18
18
  private scan;
19
- sync(integration: EnvarIntegration, payments: {
20
- network: string;
21
- pay_to: string;
22
- facilitator_url: string;
23
- }[]): Promise<void>;
19
+ sync(integration: EnvarIntegration): Promise<void>;
24
20
  }
@@ -75,7 +75,7 @@ export class SkillDiscovery {
75
75
  throw Error("inventory");
76
76
  return list;
77
77
  }
78
- async sync(integration, payments) {
78
+ async sync(integration) {
79
79
  if (Date.now() >= this.nextScan) {
80
80
  this.nextScan = Date.now() + 15000;
81
81
  try {
@@ -121,9 +121,6 @@ export class SkillDiscovery {
121
121
  card_url: this.origin + "/.well-known/agent-card.json",
122
122
  state: this.state,
123
123
  skills: skills.slice(0, 128),
124
- payments: payments
125
- .filter((p, i, all) => all.findIndex((x) => JSON.stringify(x) === JSON.stringify(p)) === i)
126
- .slice(0, 8),
127
124
  });
128
125
  for (const request of result) {
129
126
  const skill = this.cached.find((s) => s.name === request.name && s.digest === request.digest && s.supported);
@@ -1,4 +1,4 @@
1
- /** Opt-in seller gate for a captured Envar account-balance claim. No x402 or MPP impersonation. */
1
+ /** Seller gate for one captured Envar account-balance claim. */
2
2
  import { digest } from './config.js';
3
3
  import { CommerceError } from './types.js';
4
4
  const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
@@ -9,15 +9,8 @@ export interface EnvarPolicy {
9
9
  acceptUpdates: boolean;
10
10
  allowedServices: string[];
11
11
  allowedUpstreamOrigins: string[];
12
- allowedX402: {
13
- network: string;
14
- asset: string;
15
- payTo: string;
16
- facilitatorOrigin: string;
17
- }[];
18
- allowedMppAccounts: string[];
19
12
  /** Explicit seller account references allowed to accept platform balance. */
20
- allowedBalanceAccounts?: string[];
13
+ allowedBalanceAccounts: string[];
21
14
  }
22
15
  export interface EnvarCandidate {
23
16
  id: string;
@@ -49,20 +42,6 @@ export interface EnvarIntegrationOptions {
49
42
  responseLimitBytes?: number;
50
43
  now?: () => number;
51
44
  }
52
- export interface EnvarX402Evidence {
53
- network: string;
54
- asset: string;
55
- payer: string;
56
- recipient: string;
57
- amount: string;
58
- nonce: string;
59
- transaction: string;
60
- }
61
- export interface EnvarMppEvidence {
62
- reference: string;
63
- amount: string;
64
- currency: string;
65
- }
66
45
  /** Same frozen fingerprint as CommerceStore.registerCatalog; callers must read actual persisted state. */
67
46
  export declare function envarServiceProofs(config: CommerceConfig): EnvarApplicationProof["services"];
68
47
  export declare class EnvarIntegration {
@@ -110,16 +89,6 @@ export declare class EnvarIntegration {
110
89
  orderId: string;
111
90
  kind: EnvarStatusKind;
112
91
  }): string;
113
- enqueueX402Payment(dedupeKey: string, report: {
114
- orderId: string;
115
- quote: PriceQuote;
116
- payment: EnvarX402Evidence;
117
- }): string;
118
- enqueueMppPayment(dedupeKey: string, report: {
119
- orderId: string;
120
- quote: PriceQuote;
121
- payment: EnvarMppEvidence;
122
- }): string;
123
92
  queueStatus(): {
124
93
  pending: number;
125
94
  sent: number;
@@ -3,9 +3,9 @@ import { randomUUID } from "node:crypto";
3
3
  import { chmodSync, closeSync, existsSync, fsyncSync, lstatSync, mkdirSync, openSync, readFileSync, renameSync, unlinkSync, writeFileSync, } from "node:fs";
4
4
  import { join, resolve } from "node:path";
5
5
  import { DatabaseSync } from "node:sqlite";
6
- import { atomic, currencyOf, digest, loadCommerceConfig } from "./config.js";
6
+ import { digest, loadCommerceConfig } from "./config.js";
7
7
  import { CommerceError, } from "./types.js";
8
- const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i, HASH = /^0x[0-9a-f]{64}$/i, ADDRESS = /^0x[0-9a-f]{40}$/i, ID = /^[a-z][a-z0-9-]{0,63}$/;
8
+ const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i, ID = /^[a-z][a-z0-9-]{0,63}$/;
9
9
  const STATUS_KINDS = [
10
10
  "seller_started",
11
11
  "seller_completed",
@@ -122,20 +122,6 @@ export class EnvarIntegration {
122
122
  this.policy.allowedServices.some((id) => !ID.test(id)))
123
123
  fail("envar_policy", "Configure valid local Agent and service IDs");
124
124
  this.policy.allowedUpstreamOrigins = this.policy.allowedUpstreamOrigins.map((url) => origin(url, true));
125
- this.policy.allowedX402 = this.policy.allowedX402.map((entry) => {
126
- if (!/^eip155:[1-9][0-9]*$/.test(entry.network) ||
127
- !ADDRESS.test(entry.asset) ||
128
- !ADDRESS.test(entry.payTo))
129
- fail("envar_policy", "Configure explicit receiving identities");
130
- return {
131
- ...entry,
132
- asset: entry.asset.toLowerCase(),
133
- payTo: entry.payTo.toLowerCase(),
134
- facilitatorOrigin: origin(entry.facilitatorOrigin),
135
- };
136
- });
137
- if (this.policy.allowedMppAccounts.some((id) => !ID.test(id)))
138
- fail("envar_policy", "Configure explicit merchant account references");
139
125
  this.policy.allowedBalanceAccounts ??= [];
140
126
  if (this.policy.allowedBalanceAccounts.some((id) => !ID.test(id)))
141
127
  fail("envar_policy", "Configure explicit balance account references");
@@ -357,16 +343,7 @@ export class EnvarIntegration {
357
343
  if (!this.policy.allowedUpstreamOrigins.includes(new URL(service.execution.cardUrl).origin))
358
344
  fail("envar_upstream_denied", "Upstream origin is outside the local update grant");
359
345
  for (const profile of Object.values(config.paymentProfiles)) {
360
- if (profile.adapter === "x402") {
361
- if (!this.policy.allowedX402.some((a) => a.network === profile.network &&
362
- a.asset === profile.asset.toLowerCase() &&
363
- a.payTo === profile.payTo.toLowerCase() &&
364
- a.facilitatorOrigin === new URL(profile.facilitatorUrl).origin))
365
- fail("envar_recipient_denied", "Receiving identity or facilitator is outside the local update grant");
366
- }
367
- else if (profile.adapter === "mpp" && !this.policy.allowedMppAccounts.includes(profile.accountRef))
368
- fail("envar_merchant_denied", "Merchant account is outside the local update grant");
369
- else if (profile.adapter === "envar_balance" && !this.policy.allowedBalanceAccounts.includes(profile.accountRef))
346
+ if (!this.policy.allowedBalanceAccounts.includes(profile.accountRef))
370
347
  fail("envar_balance_denied", "Balance receiving account is outside the local update grant");
371
348
  }
372
349
  const prior = this.db
@@ -557,62 +534,6 @@ export class EnvarIntegration {
557
534
  payload: {},
558
535
  });
559
536
  }
560
- enqueueX402Payment(dedupeKey, report) {
561
- uuid(report.orderId);
562
- const { quote, payment: p } = report, profile = quote.paymentProfile;
563
- if (!profile ||
564
- profile.adapter !== "x402" ||
565
- p.network !== profile.network ||
566
- p.asset.toLowerCase() !== profile.asset.toLowerCase() ||
567
- p.recipient.toLowerCase() !== profile.payTo.toLowerCase() ||
568
- p.amount !== quote.amount ||
569
- quote.currency !== currencyOf(profile) ||
570
- quote.recipient?.toLowerCase() !== profile.payTo.toLowerCase() ||
571
- !ADDRESS.test(p.payer) ||
572
- !HASH.test(p.nonce) ||
573
- !HASH.test(p.transaction))
574
- fail("envar_payment_binding", "Report the original x402 transaction matching the frozen quote");
575
- atomic(p.amount);
576
- return this.enqueue(`report:${dedupeKey}`, "report", {
577
- order_id: report.orderId,
578
- kind: "payment_observed",
579
- payload: {
580
- protocol: "x402",
581
- payment: {
582
- network: p.network,
583
- asset: p.asset.toLowerCase(),
584
- payer: p.payer.toLowerCase(),
585
- recipient: p.recipient.toLowerCase(),
586
- amount: p.amount,
587
- nonce: p.nonce.toLowerCase(),
588
- transaction: p.transaction.toLowerCase(),
589
- },
590
- },
591
- });
592
- }
593
- enqueueMppPayment(dedupeKey, report) {
594
- uuid(report.orderId);
595
- const { quote, payment: p } = report;
596
- if (quote.paymentProfile?.adapter !== "mpp" ||
597
- p.amount !== quote.amount ||
598
- p.currency !== quote.currency ||
599
- p.currency !== "usd" ||
600
- !/^pi_[A-Za-z0-9]{1,240}$/.test(p.reference))
601
- fail("envar_payment_binding", "Report the original Stripe PaymentIntent matching the frozen quote");
602
- atomic(p.amount);
603
- return this.enqueue(`report:${dedupeKey}`, "report", {
604
- order_id: report.orderId,
605
- kind: "payment_observed",
606
- payload: {
607
- protocol: "mpp",
608
- payment: {
609
- reference: p.reference,
610
- amount: p.amount,
611
- currency: p.currency,
612
- },
613
- },
614
- });
615
- }
616
537
  queueStatus() {
617
538
  this.open();
618
539
  const count = (state) => Number(this.db
@@ -1,8 +1,7 @@
1
1
  export { inspectLocalAlerts, type LocalAlertOptions } from './alerts.js';
2
2
  export type InspectionView = 'attention' | 'refund-review';
3
- /** Local owner-only diagnostics: never opens a signer, vault, provider or network. */
4
3
  export declare function inspectLedger(path: string, after?: number, limit?: number, view?: InspectionView): {
5
- role: string;
4
+ role: "seller";
6
5
  view: InspectionView;
7
6
  total: number;
8
7
  attentionCount: number;
@@ -1,37 +1,34 @@
1
+ /** Owner-only, read-only inspection of original seller orders. */
1
2
  import { DatabaseSync } from 'node:sqlite';
2
3
  import { lstatSync } from 'node:fs';
3
4
  import { CommerceError } from './types.js';
4
5
  export { inspectLocalAlerts } from './alerts.js';
5
- /** Local owner-only diagnostics: never opens a signer, vault, provider or network. */
6
+ const ATTENTION = "payment_state IN ('settling','unknown') OR execution_state IN ('dispatching','unknown')";
7
+ const REFUND_REVIEW = "payment_state='confirmed' AND execution_state='failed'";
6
8
  export function inspectLedger(path, after = 0, limit = 100, view = 'attention') {
7
9
  const stat = lstatSync(path);
8
10
  if (!stat.isFile() || stat.isSymbolicLink() || (stat.mode & 0o077) !== 0 || (process.getuid && stat.uid !== process.getuid()))
9
11
  throw new CommerceError('private_file', 'Inspect an existing ledger owned only by the current user');
10
12
  if (!Number.isSafeInteger(after) || after < 0 || !Number.isSafeInteger(limit) || limit < 1 || limit > 100)
11
- throw new CommerceError('inspection_cursor', 'Use a nonnegative row cursor and a limit from1 to100');
13
+ throw new CommerceError('inspection_cursor', 'Use a nonnegative row cursor and a limit from 1 to 100');
12
14
  if (!['attention', 'refund-review'].includes(view))
13
15
  throw new CommerceError('inspection_view', 'Use attention or refund-review inspection');
14
16
  const db = new DatabaseSync(path, { readOnly: true });
15
17
  try {
16
18
  db.exec('PRAGMA query_only=ON; PRAGMA busy_timeout=1000');
17
- const names = new Set(db.prepare("SELECT name FROM sqlite_master WHERE type='table'").all().map(row => row.name));
18
- const role = names.has('buyer_purchases') ? 'buyer' : names.has('commerce_orders') ? 'seller' : null;
19
- if (!role)
20
- throw new CommerceError('store_version', 'Unrecognized commerce ledger');
21
- const meta = role === 'buyer' ? 'buyer_meta' : 'commerce_meta', version = role === 'buyer' ? 1 : 2;
22
- if (db.prepare(`SELECT version FROM ${meta}`).get()?.version !== version)
23
- throw new CommerceError('store_version', 'Unsupported commerce ledger version');
24
- const table = role === 'buyer' ? 'buyer_purchases' : 'commerce_orders';
25
- const total = Number(db.prepare(`SELECT COUNT(*) AS n FROM ${table}`).get().n);
26
- const attention = role === 'buyer' ? "json_extract(record_json,'$.state') IN ('signing','submitted','unknown') OR json_extract(record_json,'$.paymentState') IN ('reserved','unknown') OR json_extract(record_json,'$.executionState')='unknown'" : "payment_state IN ('settling','unknown') OR execution_state IN ('dispatching','unknown')";
27
- const refundReview = role === 'buyer' ? "json_extract(record_json,'$.paymentState')='confirmed' AND json_extract(record_json,'$.executionState')='failed'" : "payment_state='confirmed' AND execution_state='failed'";
28
- const filter = view === 'refund-review' ? refundReview : attention;
29
- const selected = role === 'buyer' ? "rowid AS cursor,id,json_extract(record_json,'$.paymentState') AS payment_state,json_extract(record_json,'$.executionState') AS execution_state,json_extract(record_json,'$.task.id') AS task_id" : "rowid AS cursor,id,payment_state,execution_state,task_id";
30
- const rows = db.prepare(`SELECT ${selected} FROM ${table} WHERE rowid>? AND (${filter}) ORDER BY rowid LIMIT ?`).all(after, limit + 1);
31
- const page = rows.slice(0, limit).map(row => ({ cursor: Number(row.cursor), id: String(row.id), paymentState: String(row.payment_state), executionState: String(row.execution_state), taskId: typeof row.task_id === 'string' ? row.task_id : null, ...(view === 'refund-review' ? { reason: 'confirmed_payment_failed_execution' } : {}) }));
32
- const attentionCount = Number(db.prepare(`SELECT COUNT(*) AS n FROM ${table} WHERE ${attention}`).get().n);
33
- const refundReviewCount = Number(db.prepare(`SELECT COUNT(*) AS n FROM ${table} WHERE ${refundReview}`).get().n);
34
- return { role, view, total, attentionCount, refundReviewCount, items: page, nextCursor: page.at(-1)?.cursor ?? after, hasMore: rows.length > limit, paymentPerformed: false, refundPerformed: false };
19
+ if (db.prepare('SELECT version FROM commerce_meta').get()?.version !== 2)
20
+ throw new CommerceError('store_version', 'Unsupported seller ledger version');
21
+ const total = Number(db.prepare('SELECT COUNT(*) AS n FROM commerce_orders').get().n);
22
+ const filter = view === 'refund-review' ? REFUND_REVIEW : ATTENTION;
23
+ const rows = db.prepare(`SELECT rowid AS cursor,id,payment_state,execution_state,task_id FROM commerce_orders WHERE rowid>? AND (${filter}) ORDER BY rowid LIMIT ?`).all(after, limit + 1);
24
+ const page = rows.slice(0, limit).map(row => ({
25
+ cursor: Number(row.cursor), id: String(row.id), paymentState: String(row.payment_state),
26
+ executionState: String(row.execution_state), taskId: typeof row.task_id === 'string' ? row.task_id : null,
27
+ ...(view === 'refund-review' ? { reason: 'confirmed_payment_failed_execution' } : {}),
28
+ }));
29
+ const attentionCount = Number(db.prepare(`SELECT COUNT(*) AS n FROM commerce_orders WHERE ${ATTENTION}`).get().n);
30
+ const refundReviewCount = Number(db.prepare(`SELECT COUNT(*) AS n FROM commerce_orders WHERE ${REFUND_REVIEW}`).get().n);
31
+ return { role: 'seller', view, total, attentionCount, refundReviewCount, items: page, nextCursor: page.at(-1)?.cursor ?? after, hasMore: rows.length > limit, paymentPerformed: false, refundPerformed: false };
35
32
  }
36
33
  finally {
37
34
  db.close();
@@ -7,6 +7,5 @@ export declare function buildQuote(config: CommerceConfig, args: {
7
7
  input: Input;
8
8
  now?: Date;
9
9
  ttlSeconds?: number;
10
- stripeRecipient?: string;
11
10
  }): Readonly<PriceQuote>;
12
11
  export declare function assertQuoteRequest(quote: Readonly<PriceQuote>, caller: string, messageId: string, input: Input, now?: Date): void;
@@ -33,9 +33,7 @@ export function buildQuote(config, args) {
33
33
  if (amount >= 2n ** 256n)
34
34
  throw new CommerceError('invalid_amount', 'Quoted amount is too large');
35
35
  const profile = offer.paymentProfile ? config.paymentProfiles[offer.paymentProfile] : null;
36
- const recipient = profile?.adapter === 'x402' ? profile.payTo : profile?.adapter === 'envar_balance' ? profile.accountRef : profile ? args.stripeRecipient : null;
37
- if (profile?.adapter === 'mpp' && !recipient)
38
- throw new CommerceError('merchant_not_ready', 'Resolve and verify the Stripe merchant before quoting');
36
+ const recipient = profile?.accountRef ?? null;
39
37
  const now = args.now ?? new Date();
40
38
  const ttl = args.ttlSeconds ?? 600;
41
39
  if (!Number.isInteger(ttl) || ttl < 1 || ttl > 3600 || !Number.isFinite(now.getTime()))
@@ -5,8 +5,6 @@ export * from './upstream.js';
5
5
  export * from './skill-gate.js';
6
6
  export * from './native-skills.js';
7
7
  export * from './http.js';
8
- export * from './x402.js';
9
8
  export * from './envar-balance.js';
10
9
  export * from './vault.js';
11
- export * from './chain.js';
12
10
  export * from './external-agent.js';
@@ -5,8 +5,6 @@ export * from './upstream.js';
5
5
  export * from './skill-gate.js';
6
6
  export * from './native-skills.js';
7
7
  export * from './http.js';
8
- export * from './x402.js';
9
8
  export * from './envar-balance.js';
10
9
  export * from './vault.js';
11
- export * from './chain.js';
12
10
  export * from './external-agent.js';
@@ -23,7 +23,6 @@ export interface CommerceServerOptions {
23
23
  headers: Record<string, string>;
24
24
  }>;
25
25
  };
26
- stripeRecipientFor?: (accountRef: string) => string | undefined;
27
26
  ownershipChallenges?: Readonly<Record<string, string>>;
28
27
  onError?: (code: string, orderId?: string) => void;
29
28
  }
@@ -233,9 +233,8 @@ export class CommerceServer {
233
233
  const frozen = this.store.catalogHistory().find(entry => entry.service.id === service.id && entry.service.revision === service.revision);
234
234
  if (!frozen)
235
235
  throw new CommerceError('unknown_revision', 'Service revision is not registered');
236
- const config = { ...this.currentConfig, paymentProfiles: frozen.profiles, services: [frozen.service] }, offer = frozen.service.offers.find(o => o.id === offerId);
237
- const profile = offer.paymentProfile ? frozen.profiles[offer.paymentProfile] : undefined;
238
- return buildQuote(config, { serviceId: service.id, offerId, caller, messageId, input, ...(profile?.adapter === 'mpp' ? { stripeRecipient: this.options.stripeRecipientFor?.(profile.accountRef) } : {}) });
236
+ const config = { ...this.currentConfig, paymentProfiles: frozen.profiles, services: [frozen.service] };
237
+ return buildQuote(config, { serviceId: service.id, offerId, caller, messageId, input });
239
238
  }
240
239
  async handle(request) {
241
240
  const url = new URL(request.url);
@@ -296,9 +296,11 @@ export async function setupAgent(options) {
296
296
  services: [],
297
297
  });
298
298
  privateJson(join(staging, "seller.json"), config);
299
+ writeFileSync(join(staging, "vault.key"), randomBytes(32), { mode: 0o600, flag: "wx" });
299
300
  privateJson(join(staging, "credentials.json"), {
300
301
  callers: { [access]: "owner" },
301
302
  upstreams: {},
303
+ vaultKeyFile: join(directory, "vault.key"),
302
304
  skills: {
303
305
  framework,
304
306
  ...(probe.command ? { command: probe.command } : {}),
@@ -328,8 +330,7 @@ export async function setupAgent(options) {
328
330
  acceptUpdates: true,
329
331
  allowedServices: selected,
330
332
  allowedUpstreamOrigins: [origin],
331
- allowedX402: [],
332
- allowedMppAccounts: [],
333
+ allowedBalanceAccounts: ["envar"],
333
334
  },
334
335
  stateDirectory: join(directory, "envar-state"),
335
336
  configDirectory: join(directory, "envar-configs"),
@@ -363,7 +364,7 @@ export async function setupAgent(options) {
363
364
  ];
364
365
  const start = join(directory, "start.mjs");
365
366
  writeFileSync(join(staging, "start.mjs"), `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, flag: "wx" });
366
- writeFileSync(join(staging, "README.md"), `# ${bundle.agentName}\n\nStart: node ${JSON.stringify(start)}\n\nKeep the terminal, Hermes, and HTTPS tunnel running. Import connection.json in the Envar setup page, then check the connection. Choose a skill and save a free service. Request publication; this process applies the selected service automatically. Return to Envar and check/publish.\n\nOnly free service updates are authorized. Receiving and payment setup is separate. Platform token expires ${bundle.expiresAt}. Keep the original files and ledger when restarting.\n`, { mode: 0o600, flag: "wx" });
367
+ writeFileSync(join(staging, "README.md"), `# ${bundle.agentName}\n\n1. Start: node ${JSON.stringify(start)}\n2. Keep this process, your Agent and the HTTPS tunnel running.\n3. In Envar, import connection.json and select Check connection.\n4. Choose a Skill. You can publish it free immediately.\n5. For a paid Skill, request Envar balance receiving approval in Envar, then enter the USD price. This setup already allows the local receiving reference \"envar\"; there is no chain wallet or payment protocol to configure.\n6. Select Test service, then Publish after Envar confirms the live unpaid quote.\n\nThe Envar Agent token expires ${bundle.expiresAt}. Keep the original files and ledger when restarting. Never share credentials.json, envar.token or vault.key.\n`, { mode: 0o600, flag: "wx" });
367
368
  renameSync(staging, directory);
368
369
  staging = undefined;
369
370
  output(`\nReady. Start the local service:\n${process.execPath} ${JSON.stringify(start)}\n\nThen import this connection file in Envar:\n${join(directory, "connection.json")}\n\nKeep the service and HTTPS tunnel online. No task or payment was performed.`);