@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.
- package/README.md +18 -63
- package/dist/commerce/alerts.d.ts +5 -15
- package/dist/commerce/alerts.js +21 -77
- package/dist/commerce/card.js +1 -1
- package/dist/commerce/cli.js +17 -145
- package/dist/commerce/config.d.ts +1 -4
- package/dist/commerce/config.js +4 -82
- package/dist/commerce/configure.d.ts +0 -1
- package/dist/commerce/configure.js +0 -80
- package/dist/commerce/discovery.d.ts +1 -5
- package/dist/commerce/discovery.js +1 -4
- package/dist/commerce/envar-balance.js +1 -1
- package/dist/commerce/envar.d.ts +1 -32
- package/dist/commerce/envar.js +3 -82
- package/dist/commerce/operations.d.ts +1 -2
- package/dist/commerce/operations.js +17 -20
- package/dist/commerce/pricing.d.ts +0 -1
- package/dist/commerce/pricing.js +1 -3
- package/dist/commerce/runtime.d.ts +0 -2
- package/dist/commerce/runtime.js +0 -2
- package/dist/commerce/server.d.ts +0 -1
- package/dist/commerce/server.js +2 -3
- package/dist/commerce/setup.js +4 -3
- package/dist/commerce/types.d.ts +0 -59
- package/examples/envar-integration-policy.json +1 -9
- package/examples/seller.json +143 -26
- package/package.json +3 -16
- package/schemas/envarpay.config.schema.json +1 -80
- package/dist/commerce/buyer-store.d.ts +0 -101
- package/dist/commerce/buyer-store.js +0 -168
- package/dist/commerce/buyer.d.ts +0 -138
- package/dist/commerce/buyer.js +0 -775
- package/dist/commerce/chain.d.ts +0 -24
- package/dist/commerce/chain.js +0 -196
- package/dist/commerce/client-runtime.d.ts +0 -8
- package/dist/commerce/client-runtime.js +0 -7
- package/dist/commerce/management.d.ts +0 -14
- package/dist/commerce/management.js +0 -140
- package/dist/commerce/mpp-client.d.ts +0 -70
- package/dist/commerce/mpp-client.js +0 -29
- package/dist/commerce/mpp-runtime.d.ts +0 -4
- package/dist/commerce/mpp-runtime.js +0 -4
- package/dist/commerce/mpp.d.ts +0 -53
- package/dist/commerce/mpp.js +0 -163
- package/dist/commerce/peer-proxy.d.ts +0 -44
- package/dist/commerce/peer-proxy.js +0 -673
- package/dist/commerce/stripe-issuer.d.ts +0 -41
- package/dist/commerce/stripe-issuer.js +0 -351
- package/dist/commerce/stripe-provider.d.ts +0 -11
- package/dist/commerce/stripe-provider.js +0 -132
- package/dist/commerce/x402.d.ts +0 -38
- package/dist/commerce/x402.js +0 -181
- package/examples/buyer-policy.json +0 -18
- package/schemas/buyer-policy.schema.json +0 -253
package/dist/commerce/config.js
CHANGED
|
@@ -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.
|
|
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
|
|
100
|
+
let high;
|
|
117
101
|
if (offer.pricing.kind === 'fixed')
|
|
118
|
-
|
|
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 (
|
|
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,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
|
|
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
|
|
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
|
-
/**
|
|
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;
|
package/dist/commerce/envar.d.ts
CHANGED
|
@@ -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
|
|
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;
|
package/dist/commerce/envar.js
CHANGED
|
@@ -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 {
|
|
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,
|
|
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.
|
|
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:
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
const
|
|
28
|
-
const
|
|
29
|
-
|
|
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;
|
package/dist/commerce/pricing.js
CHANGED
|
@@ -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?.
|
|
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';
|
package/dist/commerce/runtime.js
CHANGED
|
@@ -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
|
}
|
package/dist/commerce/server.js
CHANGED
|
@@ -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] }
|
|
237
|
-
|
|
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);
|
package/dist/commerce/setup.js
CHANGED
|
@@ -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
|
-
|
|
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\
|
|
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.`);
|