@envarai/envarpay 0.2.0-alpha.8 → 2026.1025.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -56
- package/dist/commerce/agent-setup.d.ts +19 -0
- package/dist/commerce/agent-setup.js +210 -0
- 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 +74 -137
- package/dist/commerce/config.d.ts +1 -4
- package/dist/commerce/config.js +5 -81
- package/dist/commerce/configure.d.ts +2 -0
- package/dist/commerce/configure.js +78 -0
- package/dist/commerce/discovery.d.ts +20 -0
- package/dist/commerce/discovery.js +162 -0
- package/dist/commerce/envar-balance.d.ts +25 -0
- package/dist/commerce/envar-balance.js +65 -0
- package/dist/commerce/envar-runtime.d.ts +1 -0
- package/dist/commerce/envar-runtime.js +7 -0
- package/dist/commerce/envar.d.ts +13 -31
- package/dist/commerce/envar.js +35 -83
- package/dist/commerce/external-agent.d.ts +26 -0
- package/dist/commerce/external-agent.js +129 -0
- package/dist/commerce/external-events.d.ts +76 -0
- package/dist/commerce/external-events.js +273 -0
- package/dist/commerce/external-mcp.d.ts +10 -0
- package/dist/commerce/external-mcp.js +199 -0
- package/dist/commerce/external-oauth.d.ts +12 -0
- package/dist/commerce/external-oauth.js +230 -0
- package/dist/commerce/external-runtime.d.ts +25 -0
- package/dist/commerce/external-runtime.js +244 -0
- package/dist/commerce/managed.d.ts +3 -0
- package/dist/commerce/managed.js +143 -0
- package/dist/commerce/native-inventory.d.ts +9 -0
- package/dist/commerce/native-inventory.js +75 -0
- package/dist/commerce/native-skills.d.ts +8 -0
- package/dist/commerce/native-skills.js +11 -8
- package/dist/commerce/openclaw-setup.d.ts +28 -0
- package/dist/commerce/openclaw-setup.js +43 -0
- 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 +2 -2
- package/dist/commerce/runtime.js +2 -2
- package/dist/commerce/server.d.ts +0 -1
- package/dist/commerce/server.js +2 -3
- package/dist/commerce/setup.d.ts +25 -0
- package/dist/commerce/setup.js +379 -0
- package/dist/commerce/store.d.ts +11 -1
- package/dist/commerce/store.js +36 -2
- package/dist/commerce/types.d.ts +1 -56
- package/examples/envar-integration-policy.json +1 -9
- package/examples/seller.json +143 -26
- package/package.json +11 -20
- package/runtime/hermes-setup-probe.py +109 -0
- package/runtime/hermes-skill-inventory.py +108 -0
- package/schemas/envarpay.config.schema.json +7 -73
- 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 -146
- 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 -37
- package/dist/commerce/x402.js +0 -174
- package/dist/index.d.ts +0 -95
- package/dist/index.js +0 -163
- package/examples/buyer-policy.json +0 -18
- package/schemas/buyer-policy.schema.json +0 -253
- /package/{examples → runtime}/native-skill-runner.py +0 -0
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { readSkillInventory } from "./native-inventory.js";
|
|
2
|
+
import { execFile } from "node:child_process";
|
|
3
|
+
import { promisify } from "node:util";
|
|
4
|
+
import { readFileSync, writeFileSync, existsSync, mkdtempSync, mkdirSync, renameSync, rmSync, readdirSync, lstatSync, } from "node:fs";
|
|
5
|
+
import { createHash } from 'node:crypto';
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
import { createRequire } from "node:module";
|
|
8
|
+
import { prepareTextSkill } from "./setup.js";
|
|
9
|
+
import { readInstalledSkills } from "./native-skills.js";
|
|
10
|
+
const exec = promisify(execFile);
|
|
11
|
+
function packageDigest(root) {
|
|
12
|
+
const files = [];
|
|
13
|
+
let size = 0;
|
|
14
|
+
function walk(folder, prefix = '') {
|
|
15
|
+
if (prefix.split('/').length > 12)
|
|
16
|
+
throw Error('skill_size');
|
|
17
|
+
for (const name of readdirSync(folder).sort()) {
|
|
18
|
+
const path = join(folder, name), stat = lstatSync(path);
|
|
19
|
+
if (stat.isSymbolicLink())
|
|
20
|
+
throw Error('skill_symlink');
|
|
21
|
+
if (stat.isDirectory()) {
|
|
22
|
+
walk(path, prefix + name + '/');
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
if (!stat.isFile() || stat.size > 512 * 1024 || (size += stat.size) > 1024 * 1024 || files.length >= 128)
|
|
26
|
+
throw Error('skill_size');
|
|
27
|
+
files.push([prefix + name, createHash('sha256').update(readFileSync(path)).digest('hex')]);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
walk(root);
|
|
31
|
+
files.sort((a, b) => a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0);
|
|
32
|
+
const json = JSON.stringify(files).replace(/[\u007f-\uffff]/g, c => '\\u' + c.charCodeAt(0).toString(16).padStart(4, '0'));
|
|
33
|
+
return createHash('sha256').update(json).digest('hex');
|
|
34
|
+
}
|
|
35
|
+
export class SkillDiscovery {
|
|
36
|
+
settings;
|
|
37
|
+
discovery;
|
|
38
|
+
origin;
|
|
39
|
+
enabled;
|
|
40
|
+
cached = [];
|
|
41
|
+
nextScan = 0;
|
|
42
|
+
state = "ready";
|
|
43
|
+
path;
|
|
44
|
+
constructor(settings, discovery, origin) {
|
|
45
|
+
this.settings = settings;
|
|
46
|
+
this.discovery = discovery;
|
|
47
|
+
this.origin = origin;
|
|
48
|
+
this.path = join(settings.skillsDirectory, ".source-digests.json");
|
|
49
|
+
this.enabled = existsSync(this.path)
|
|
50
|
+
? JSON.parse(readFileSync(this.path, "utf8"))
|
|
51
|
+
: {};
|
|
52
|
+
}
|
|
53
|
+
async scan() {
|
|
54
|
+
if (this.discovery.roots && !this.settings.dockerContainer)
|
|
55
|
+
return readSkillInventory(this.discovery.roots);
|
|
56
|
+
const code = readFileSync(new URL("../../runtime/hermes-skill-inventory.py", import.meta.url), "utf8");
|
|
57
|
+
const args = ["-c", code, this.discovery.hermesHome];
|
|
58
|
+
const { stdout } = this.settings.dockerContainer
|
|
59
|
+
? await exec("docker", [
|
|
60
|
+
"exec",
|
|
61
|
+
this.settings.dockerContainer,
|
|
62
|
+
this.settings.python,
|
|
63
|
+
...args,
|
|
64
|
+
], { timeout: 30000, maxBuffer: 1024 * 1024 })
|
|
65
|
+
: await exec(this.settings.python, args, {
|
|
66
|
+
timeout: 30000,
|
|
67
|
+
maxBuffer: 1024 * 1024,
|
|
68
|
+
});
|
|
69
|
+
const list = JSON.parse(stdout);
|
|
70
|
+
if (!Array.isArray(list) ||
|
|
71
|
+
list.length > 128 ||
|
|
72
|
+
list.some((s) => !/^[a-z][a-z0-9-]{0,63}$/.test(s.name) ||
|
|
73
|
+
!/^[0-9a-f]{64}$/.test(s.digest) ||
|
|
74
|
+
typeof s.path !== "string"))
|
|
75
|
+
throw Error("inventory");
|
|
76
|
+
return list;
|
|
77
|
+
}
|
|
78
|
+
async sync(integration) {
|
|
79
|
+
if (Date.now() >= this.nextScan) {
|
|
80
|
+
this.nextScan = Date.now() + 15000;
|
|
81
|
+
try {
|
|
82
|
+
this.cached = await this.scan();
|
|
83
|
+
this.state = "ready";
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
this.state = this.settings.dockerContainer
|
|
87
|
+
? "agent_unavailable"
|
|
88
|
+
: "scan_failed";
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
const active = new Set(readdirSync(this.settings.skillsDirectory, { withFileTypes: true })
|
|
92
|
+
.filter((x) => x.isDirectory() && !x.name.startsWith("."))
|
|
93
|
+
.map((x) => x.name));
|
|
94
|
+
for (const skill of this.cached)
|
|
95
|
+
if (active.has(skill.name) && !this.enabled[skill.name])
|
|
96
|
+
this.enabled[skill.name] = skill.digest;
|
|
97
|
+
if (this.state === "ready")
|
|
98
|
+
writeFileSync(this.path, JSON.stringify(this.enabled), { mode: 0o600 });
|
|
99
|
+
const skills = this.cached.map((s) => ({
|
|
100
|
+
name: s.name,
|
|
101
|
+
digest: s.digest,
|
|
102
|
+
description: s.description,
|
|
103
|
+
status: !s.supported
|
|
104
|
+
? "requires_adapter"
|
|
105
|
+
: active.has(s.name)
|
|
106
|
+
? this.enabled[s.name] && this.enabled[s.name] !== s.digest
|
|
107
|
+
? "updated"
|
|
108
|
+
: "enabled"
|
|
109
|
+
: "available",
|
|
110
|
+
}));
|
|
111
|
+
for (const name of active)
|
|
112
|
+
if (!skills.some((s) => s.name === name))
|
|
113
|
+
skills.push({
|
|
114
|
+
name,
|
|
115
|
+
digest: this.enabled[name] ?? "0".repeat(64),
|
|
116
|
+
description: "",
|
|
117
|
+
status: "removed",
|
|
118
|
+
});
|
|
119
|
+
const result = await integration.reportRuntime({
|
|
120
|
+
version: createRequire(import.meta.url)("../../package.json").version,
|
|
121
|
+
card_url: this.origin + "/.well-known/agent-card.json",
|
|
122
|
+
state: this.state,
|
|
123
|
+
skills: skills.slice(0, 128),
|
|
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
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { type PriceQuote } from './types.js';
|
|
2
|
+
import { CommerceStore, type OrderRecord } from './store.js';
|
|
3
|
+
import { CredentialVault } from './vault.js';
|
|
4
|
+
export interface BalanceClaimArgs {
|
|
5
|
+
platformOrderId: string;
|
|
6
|
+
claimToken: string;
|
|
7
|
+
taskId: string;
|
|
8
|
+
quote: PriceQuote;
|
|
9
|
+
}
|
|
10
|
+
export interface EnvarBalanceGateOptions {
|
|
11
|
+
store: CommerceStore;
|
|
12
|
+
vault: CredentialVault;
|
|
13
|
+
claim: (args: BalanceClaimArgs) => Promise<Record<string, unknown>>;
|
|
14
|
+
}
|
|
15
|
+
export declare class EnvarBalanceGate {
|
|
16
|
+
private readonly options;
|
|
17
|
+
constructor(options: EnvarBalanceGateOptions);
|
|
18
|
+
private assertReceipt;
|
|
19
|
+
private settle;
|
|
20
|
+
handle(request: Request, order: OrderRecord): Promise<{
|
|
21
|
+
response: Response;
|
|
22
|
+
} | {
|
|
23
|
+
headers: Record<string, string>;
|
|
24
|
+
}>;
|
|
25
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/** Seller gate for one captured Envar account-balance claim. */
|
|
2
|
+
import { digest } from './config.js';
|
|
3
|
+
import { CommerceError } from './types.js';
|
|
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;
|
|
5
|
+
export class EnvarBalanceGate {
|
|
6
|
+
options;
|
|
7
|
+
constructor(options) {
|
|
8
|
+
this.options = options;
|
|
9
|
+
}
|
|
10
|
+
assertReceipt(order, platformOrderId, receipt) {
|
|
11
|
+
const quote = order.quote, profile = quote.paymentProfile;
|
|
12
|
+
if (profile?.adapter !== 'envar_balance')
|
|
13
|
+
throw new CommerceError('balance_profile', 'Original quote is not an Envar balance offer');
|
|
14
|
+
if (receipt.state !== 'captured' || receipt.order_id !== platformOrderId ||
|
|
15
|
+
receipt.amount !== quote.amount || receipt.currency !== 'usd' || receipt.recipient !== profile?.accountRef ||
|
|
16
|
+
receipt.quote_id !== quote.quoteId || receipt.message_id !== quote.messageId ||
|
|
17
|
+
receipt.task_id !== order.taskId ||
|
|
18
|
+
receipt.service_id !== quote.serviceId || receipt.service_revision !== quote.serviceRevision ||
|
|
19
|
+
receipt.offer_id !== quote.offerId || receipt.input_digest !== quote.inputDigest ||
|
|
20
|
+
receipt.terms_digest !== quote.termsDigest ||
|
|
21
|
+
typeof receipt.economic_id !== 'string' || !/^ledger:[0-9a-f-]{36}$/i.test(receipt.economic_id))
|
|
22
|
+
throw new CommerceError('balance_receipt_mismatch', 'Platform capture differs from the frozen service order');
|
|
23
|
+
}
|
|
24
|
+
async settle(order, attemptId, saved) {
|
|
25
|
+
if (saved.quoteId !== order.quote.quoteId)
|
|
26
|
+
throw new CommerceError('balance_recovery_conflict', 'Original balance claim belongs to another quote');
|
|
27
|
+
let receipt;
|
|
28
|
+
try {
|
|
29
|
+
receipt = await this.options.claim({ platformOrderId: saved.platformOrderId, claimToken: saved.claimToken, taskId: order.taskId, quote: order.quote });
|
|
30
|
+
this.assertReceipt(order, saved.platformOrderId, receipt);
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
this.options.store.recordSettlement(attemptId, 'unknown', {});
|
|
34
|
+
return { response: Response.json({ error: 'balance_claim_unknown', taskId: order.taskId }, { status: 503, headers: { 'Cache-Control': 'no-store' } }) };
|
|
35
|
+
}
|
|
36
|
+
this.options.store.recordSettlement(attemptId, 'confirmed', receipt);
|
|
37
|
+
return { headers: { 'ENVAR-BALANCE-RECEIPT': String(receipt.economic_id) } };
|
|
38
|
+
}
|
|
39
|
+
async handle(request, order) {
|
|
40
|
+
const quote = order.quote, profile = quote.paymentProfile;
|
|
41
|
+
if (profile?.adapter !== 'envar_balance' || quote.currency !== 'usd' || quote.recipient !== profile.accountRef)
|
|
42
|
+
throw new CommerceError('balance_profile', 'This offer does not accept Envar account balance');
|
|
43
|
+
const prior = this.options.store.paymentForOrder(order.id);
|
|
44
|
+
if (order.paymentState === 'confirmed' && prior?.receipt)
|
|
45
|
+
return { headers: { 'ENVAR-BALANCE-RECEIPT': String(prior.receipt.economic_id) } };
|
|
46
|
+
if (['settling', 'unknown'].includes(order.paymentState)) {
|
|
47
|
+
if (!prior)
|
|
48
|
+
throw new CommerceError('balance_recovery_conflict', 'Original balance attempt is missing');
|
|
49
|
+
const saved = this.options.vault.get(prior.credentialRef);
|
|
50
|
+
return this.settle(order, prior.id, saved);
|
|
51
|
+
}
|
|
52
|
+
if (Date.now() >= Date.parse(quote.expiresAt))
|
|
53
|
+
return { response: Response.json({ error: 'quote_expired' }, { status: 409, headers: { 'Cache-Control': 'no-store' } }) };
|
|
54
|
+
const platformOrderId = request.headers.get('Envar-Balance-Order') ?? '';
|
|
55
|
+
const claimToken = request.headers.get('Envar-Balance-Claim') ?? '';
|
|
56
|
+
if (!platformOrderId && !claimToken)
|
|
57
|
+
return { response: Response.json({ error: 'envar_balance_required', taskId: order.taskId, quote: { quoteId: quote.quoteId, messageId: quote.messageId, serviceId: quote.serviceId, serviceRevision: quote.serviceRevision, offerId: quote.offerId, inputDigest: quote.inputDigest, termsDigest: quote.termsDigest, amount: quote.amount, currency: quote.currency, recipient: quote.recipient, expiresAt: quote.expiresAt } }, { status: 402, headers: { 'Cache-Control': 'no-store' } }) };
|
|
58
|
+
if (!UUID.test(platformOrderId) || claimToken.length < 32 || claimToken.length > 4096 || /\s/.test(claimToken))
|
|
59
|
+
throw new CommerceError('balance_claim', 'Use the original bounded platform balance claim');
|
|
60
|
+
const credentialRef = this.options.vault.put({ platformOrderId, claimToken, quoteId: quote.quoteId });
|
|
61
|
+
const economicKey = digest({ adapter: 'envar_balance', platformOrderId });
|
|
62
|
+
const attempt = this.options.store.reservePayment(order.id, order.caller, economicKey, credentialRef);
|
|
63
|
+
return this.settle(order, attempt, { platformOrderId, claimToken, quoteId: quote.quoteId });
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -31,6 +31,7 @@ export interface EnvarSellerRuntimeOptions {
|
|
|
31
31
|
/** Local payment adapters/merchant credentials must already exist; config sync cannot create them. */
|
|
32
32
|
validateConfig?: (config: CommerceConfig) => void;
|
|
33
33
|
onError?: (code: string) => void;
|
|
34
|
+
beforeSync?: () => Promise<void>;
|
|
34
35
|
}
|
|
35
36
|
export declare class EnvarSellerRuntime {
|
|
36
37
|
private readonly options;
|
|
@@ -231,6 +231,13 @@ export class EnvarSellerRuntime {
|
|
|
231
231
|
return this.running;
|
|
232
232
|
this.running = (async () => {
|
|
233
233
|
try {
|
|
234
|
+
try {
|
|
235
|
+
await this.options.beforeSync?.();
|
|
236
|
+
}
|
|
237
|
+
catch {
|
|
238
|
+
// A discovery failure must not stall existing service updates or delivery reports.
|
|
239
|
+
this.options.onError?.("skill_discovery");
|
|
240
|
+
}
|
|
234
241
|
for (const candidate of await this.options.integration.pullCandidates())
|
|
235
242
|
await this.options.integration.applyCandidate(candidate, (c) => this.apply(c));
|
|
236
243
|
}
|
package/dist/commerce/envar.d.ts
CHANGED
|
@@ -9,13 +9,8 @@ export interface EnvarPolicy {
|
|
|
9
9
|
acceptUpdates: boolean;
|
|
10
10
|
allowedServices: string[];
|
|
11
11
|
allowedUpstreamOrigins: string[];
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
asset: string;
|
|
15
|
-
payTo: string;
|
|
16
|
-
facilitatorOrigin: string;
|
|
17
|
-
}[];
|
|
18
|
-
allowedMppAccounts: string[];
|
|
12
|
+
/** Explicit seller account references allowed to accept platform balance. */
|
|
13
|
+
allowedBalanceAccounts: string[];
|
|
19
14
|
}
|
|
20
15
|
export interface EnvarCandidate {
|
|
21
16
|
id: string;
|
|
@@ -47,20 +42,6 @@ export interface EnvarIntegrationOptions {
|
|
|
47
42
|
responseLimitBytes?: number;
|
|
48
43
|
now?: () => number;
|
|
49
44
|
}
|
|
50
|
-
export interface EnvarX402Evidence {
|
|
51
|
-
network: string;
|
|
52
|
-
asset: string;
|
|
53
|
-
payer: string;
|
|
54
|
-
recipient: string;
|
|
55
|
-
amount: string;
|
|
56
|
-
nonce: string;
|
|
57
|
-
transaction: string;
|
|
58
|
-
}
|
|
59
|
-
export interface EnvarMppEvidence {
|
|
60
|
-
reference: string;
|
|
61
|
-
amount: string;
|
|
62
|
-
currency: string;
|
|
63
|
-
}
|
|
64
45
|
/** Same frozen fingerprint as CommerceStore.registerCatalog; callers must read actual persisted state. */
|
|
65
46
|
export declare function envarServiceProofs(config: CommerceConfig): EnvarApplicationProof["services"];
|
|
66
47
|
export declare class EnvarIntegration {
|
|
@@ -81,6 +62,17 @@ export declare class EnvarIntegration {
|
|
|
81
62
|
close(): void;
|
|
82
63
|
private open;
|
|
83
64
|
private request;
|
|
65
|
+
claimBalancePayment(args: {
|
|
66
|
+
platformOrderId: string;
|
|
67
|
+
claimToken: string;
|
|
68
|
+
taskId: string;
|
|
69
|
+
quote: PriceQuote;
|
|
70
|
+
}): Promise<Record<string, unknown>>;
|
|
71
|
+
allowInstalledService(name: string): void;
|
|
72
|
+
reportRuntime(value: unknown): Promise<{
|
|
73
|
+
name: string;
|
|
74
|
+
digest: string;
|
|
75
|
+
}[]>;
|
|
84
76
|
private candidate;
|
|
85
77
|
/** Latest locally acknowledged revisions; startup recovery never requires platform connectivity. */
|
|
86
78
|
appliedCandidates(): EnvarCandidate[];
|
|
@@ -97,16 +89,6 @@ export declare class EnvarIntegration {
|
|
|
97
89
|
orderId: string;
|
|
98
90
|
kind: EnvarStatusKind;
|
|
99
91
|
}): string;
|
|
100
|
-
enqueueX402Payment(dedupeKey: string, report: {
|
|
101
|
-
orderId: string;
|
|
102
|
-
quote: PriceQuote;
|
|
103
|
-
payment: EnvarX402Evidence;
|
|
104
|
-
}): string;
|
|
105
|
-
enqueueMppPayment(dedupeKey: string, report: {
|
|
106
|
-
orderId: string;
|
|
107
|
-
quote: PriceQuote;
|
|
108
|
-
payment: EnvarMppEvidence;
|
|
109
|
-
}): string;
|
|
110
92
|
queueStatus(): {
|
|
111
93
|
pending: number;
|
|
112
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,9 @@ 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.
|
|
126
|
-
|
|
127
|
-
|
|
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");
|
|
125
|
+
this.policy.allowedBalanceAccounts ??= [];
|
|
126
|
+
if (this.policy.allowedBalanceAccounts.some((id) => !ID.test(id)))
|
|
127
|
+
fail("envar_policy", "Configure explicit balance account references");
|
|
139
128
|
this.timeoutMs = options.timeoutMs ?? 10000;
|
|
140
129
|
this.limitBytes = options.responseLimitBytes ?? 1048576;
|
|
141
130
|
if (!Number.isInteger(this.timeoutMs) ||
|
|
@@ -214,11 +203,12 @@ export class EnvarIntegration {
|
|
|
214
203
|
}
|
|
215
204
|
async request(path, payload, eventId) {
|
|
216
205
|
this.open();
|
|
217
|
-
if (![
|
|
206
|
+
if (!([
|
|
218
207
|
"/api/v1/agent-config/candidates",
|
|
219
208
|
"/api/v1/agent-config/acknowledgments",
|
|
220
209
|
"/api/v1/commerce-reports",
|
|
221
|
-
|
|
210
|
+
"/api/v1/agent-config/runtime",
|
|
211
|
+
].includes(path) || /^\/api\/v1\/commerce\/payment-claims\/[0-9a-f-]{36}\/claim$/i.test(path)))
|
|
222
212
|
fail("envar_path", "Unsupported platform integration operation");
|
|
223
213
|
const token = await this.options.token();
|
|
224
214
|
if (typeof token !== "string" ||
|
|
@@ -297,6 +287,31 @@ export class EnvarIntegration {
|
|
|
297
287
|
fail("envar_response_json", "Platform response must be an object");
|
|
298
288
|
return value;
|
|
299
289
|
}
|
|
290
|
+
async claimBalancePayment(args) {
|
|
291
|
+
uuid(args.platformOrderId);
|
|
292
|
+
if (typeof args.claimToken !== 'string' || args.claimToken.length < 32 || args.claimToken.length > 4096 || /\s/.test(args.claimToken))
|
|
293
|
+
fail('envar_balance_claim', 'Use the original bounded platform claim');
|
|
294
|
+
return this.request(`/api/v1/commerce/payment-claims/${args.platformOrderId}/claim`, {
|
|
295
|
+
claim_token: args.claimToken, task_id: args.taskId,
|
|
296
|
+
quote_id: args.quote.quoteId, message_id: args.quote.messageId,
|
|
297
|
+
service_id: args.quote.serviceId, service_revision: args.quote.serviceRevision,
|
|
298
|
+
offer_id: args.quote.offerId, input_digest: args.quote.inputDigest,
|
|
299
|
+
terms_digest: args.quote.termsDigest, amount: args.quote.amount,
|
|
300
|
+
currency: args.quote.currency, recipient: args.quote.recipient,
|
|
301
|
+
}, args.platformOrderId);
|
|
302
|
+
}
|
|
303
|
+
allowInstalledService(name) {
|
|
304
|
+
if (!ID.test(name))
|
|
305
|
+
fail('envar_policy', 'Invalid installed Skill name');
|
|
306
|
+
if (!this.policy.allowedServices.includes(name))
|
|
307
|
+
this.policy.allowedServices.push(name);
|
|
308
|
+
}
|
|
309
|
+
async reportRuntime(value) {
|
|
310
|
+
const response = await this.request('/api/v1/agent-config/runtime', value, randomUUID());
|
|
311
|
+
if (response.agent_id !== this.policy.agentId || !Array.isArray(response.enable) || response.enable.length > 128)
|
|
312
|
+
fail('envar_inventory', 'Invalid runtime acknowledgment');
|
|
313
|
+
return response.enable.filter((x) => x && ID.test(x.name) && /^[0-9a-f]{64}$/.test(x.digest));
|
|
314
|
+
}
|
|
300
315
|
candidate(value) {
|
|
301
316
|
if (!this.policy.acceptUpdates)
|
|
302
317
|
fail("envar_updates_disabled", "Local owner has not enabled platform configuration updates");
|
|
@@ -328,15 +343,8 @@ export class EnvarIntegration {
|
|
|
328
343
|
if (!this.policy.allowedUpstreamOrigins.includes(new URL(service.execution.cardUrl).origin))
|
|
329
344
|
fail("envar_upstream_denied", "Upstream origin is outside the local update grant");
|
|
330
345
|
for (const profile of Object.values(config.paymentProfiles)) {
|
|
331
|
-
if (profile.
|
|
332
|
-
|
|
333
|
-
a.asset === profile.asset.toLowerCase() &&
|
|
334
|
-
a.payTo === profile.payTo.toLowerCase() &&
|
|
335
|
-
a.facilitatorOrigin === new URL(profile.facilitatorUrl).origin))
|
|
336
|
-
fail("envar_recipient_denied", "Receiving identity or facilitator is outside the local update grant");
|
|
337
|
-
}
|
|
338
|
-
else if (!this.policy.allowedMppAccounts.includes(profile.accountRef))
|
|
339
|
-
fail("envar_merchant_denied", "Merchant account is outside the local update grant");
|
|
346
|
+
if (!this.policy.allowedBalanceAccounts.includes(profile.accountRef))
|
|
347
|
+
fail("envar_balance_denied", "Balance receiving account is outside the local update grant");
|
|
340
348
|
}
|
|
341
349
|
const prior = this.db
|
|
342
350
|
.prepare("SELECT * FROM revisions WHERE id=? OR (local_service=? AND revision=?)")
|
|
@@ -526,62 +534,6 @@ export class EnvarIntegration {
|
|
|
526
534
|
payload: {},
|
|
527
535
|
});
|
|
528
536
|
}
|
|
529
|
-
enqueueX402Payment(dedupeKey, report) {
|
|
530
|
-
uuid(report.orderId);
|
|
531
|
-
const { quote, payment: p } = report, profile = quote.paymentProfile;
|
|
532
|
-
if (!profile ||
|
|
533
|
-
profile.adapter !== "x402" ||
|
|
534
|
-
p.network !== profile.network ||
|
|
535
|
-
p.asset.toLowerCase() !== profile.asset.toLowerCase() ||
|
|
536
|
-
p.recipient.toLowerCase() !== profile.payTo.toLowerCase() ||
|
|
537
|
-
p.amount !== quote.amount ||
|
|
538
|
-
quote.currency !== currencyOf(profile) ||
|
|
539
|
-
quote.recipient?.toLowerCase() !== profile.payTo.toLowerCase() ||
|
|
540
|
-
!ADDRESS.test(p.payer) ||
|
|
541
|
-
!HASH.test(p.nonce) ||
|
|
542
|
-
!HASH.test(p.transaction))
|
|
543
|
-
fail("envar_payment_binding", "Report the original x402 transaction matching the frozen quote");
|
|
544
|
-
atomic(p.amount);
|
|
545
|
-
return this.enqueue(`report:${dedupeKey}`, "report", {
|
|
546
|
-
order_id: report.orderId,
|
|
547
|
-
kind: "payment_observed",
|
|
548
|
-
payload: {
|
|
549
|
-
protocol: "x402",
|
|
550
|
-
payment: {
|
|
551
|
-
network: p.network,
|
|
552
|
-
asset: p.asset.toLowerCase(),
|
|
553
|
-
payer: p.payer.toLowerCase(),
|
|
554
|
-
recipient: p.recipient.toLowerCase(),
|
|
555
|
-
amount: p.amount,
|
|
556
|
-
nonce: p.nonce.toLowerCase(),
|
|
557
|
-
transaction: p.transaction.toLowerCase(),
|
|
558
|
-
},
|
|
559
|
-
},
|
|
560
|
-
});
|
|
561
|
-
}
|
|
562
|
-
enqueueMppPayment(dedupeKey, report) {
|
|
563
|
-
uuid(report.orderId);
|
|
564
|
-
const { quote, payment: p } = report;
|
|
565
|
-
if (quote.paymentProfile?.adapter !== "mpp" ||
|
|
566
|
-
p.amount !== quote.amount ||
|
|
567
|
-
p.currency !== quote.currency ||
|
|
568
|
-
p.currency !== "usd" ||
|
|
569
|
-
!/^pi_[A-Za-z0-9]{1,240}$/.test(p.reference))
|
|
570
|
-
fail("envar_payment_binding", "Report the original Stripe PaymentIntent matching the frozen quote");
|
|
571
|
-
atomic(p.amount);
|
|
572
|
-
return this.enqueue(`report:${dedupeKey}`, "report", {
|
|
573
|
-
order_id: report.orderId,
|
|
574
|
-
kind: "payment_observed",
|
|
575
|
-
payload: {
|
|
576
|
-
protocol: "mpp",
|
|
577
|
-
payment: {
|
|
578
|
-
reference: p.reference,
|
|
579
|
-
amount: p.amount,
|
|
580
|
-
currency: p.currency,
|
|
581
|
-
},
|
|
582
|
-
},
|
|
583
|
-
});
|
|
584
|
-
}
|
|
585
537
|
queueStatus() {
|
|
586
538
|
this.open();
|
|
587
539
|
const count = (state) => Number(this.db
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { CommerceStore } from "./store.js";
|
|
2
|
+
import type { ExecuteOrder } from "./server.js";
|
|
3
|
+
export declare class ExternalAgentExecutor {
|
|
4
|
+
readonly store: CommerceStore;
|
|
5
|
+
readonly principal: string;
|
|
6
|
+
readonly services: readonly string[];
|
|
7
|
+
readonly execute: ExecuteOrder;
|
|
8
|
+
constructor(store: CommerceStore, principal: string, services: readonly string[]);
|
|
9
|
+
private context;
|
|
10
|
+
list(): {
|
|
11
|
+
taskId: string;
|
|
12
|
+
serviceId: string;
|
|
13
|
+
claimId: string;
|
|
14
|
+
state: "unknown" | "not_started" | "queued" | "dispatching" | "working" | "input_required" | "auth_required" | "completed" | "failed";
|
|
15
|
+
input: import("./types.js").Input;
|
|
16
|
+
paymentState: "not_required" | "quoted" | "settling" | "confirmed" | "rejected" | "unknown";
|
|
17
|
+
}[];
|
|
18
|
+
claim(taskId: string): Promise<{
|
|
19
|
+
taskId: string;
|
|
20
|
+
claimId: string;
|
|
21
|
+
input: import("./types.js").Input;
|
|
22
|
+
paymentState: "not_required" | "quoted" | "settling" | "confirmed" | "rejected" | "unknown";
|
|
23
|
+
state: "unknown" | "not_started" | "queued" | "dispatching" | "working" | "input_required" | "auth_required" | "completed" | "failed";
|
|
24
|
+
}>;
|
|
25
|
+
complete(taskId: string, claimId: string, text: string, failed?: boolean): Promise<unknown>;
|
|
26
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/** User-operated external agents claim and complete the original, payment-gated commerce Task. */
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { Task, TaskState } from "@a2a-js/sdk";
|
|
4
|
+
import { ServerCallContext } from "@a2a-js/sdk/server";
|
|
5
|
+
import { CommerceError } from "./types.js";
|
|
6
|
+
export class ExternalAgentExecutor {
|
|
7
|
+
store;
|
|
8
|
+
principal;
|
|
9
|
+
services;
|
|
10
|
+
execute;
|
|
11
|
+
constructor(store, principal, services) {
|
|
12
|
+
this.store = store;
|
|
13
|
+
this.principal = principal;
|
|
14
|
+
this.services = services;
|
|
15
|
+
if (!principal ||
|
|
16
|
+
!services.length ||
|
|
17
|
+
services.some((s) => !/^[a-z][a-z0-9-]{0,63}$/.test(s)))
|
|
18
|
+
throw new CommerceError("external_scope", "Configure one agent principal and explicit services");
|
|
19
|
+
const self = this;
|
|
20
|
+
this.execute = async function* (order, service) {
|
|
21
|
+
if (!self.services.includes(service.id) ||
|
|
22
|
+
service.execution.type !== "a2a")
|
|
23
|
+
throw new CommerceError("external_scope", "This external agent cannot execute that service");
|
|
24
|
+
self.store.prepareExternalTask(order.id, self.principal);
|
|
25
|
+
const original = await self.store.load(order.taskId, self.context(order));
|
|
26
|
+
if (!original)
|
|
27
|
+
throw new CommerceError("external_task", "Original commerce Task missing");
|
|
28
|
+
yield Task.fromJSON({
|
|
29
|
+
...Task.toJSON(original),
|
|
30
|
+
status: {
|
|
31
|
+
state: "TASK_STATE_INPUT_REQUIRED",
|
|
32
|
+
message: {
|
|
33
|
+
messageId: randomUUID(),
|
|
34
|
+
role: "ROLE_AGENT",
|
|
35
|
+
parts: [
|
|
36
|
+
{
|
|
37
|
+
text: "Waiting for the connected external agent to claim this original Task.",
|
|
38
|
+
},
|
|
39
|
+
],
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
metadata: { externalAgent: self.principal },
|
|
43
|
+
});
|
|
44
|
+
};
|
|
45
|
+
this.execute.recover = async (order) => {
|
|
46
|
+
const task = await self.store.load(order.taskId, self.context(order));
|
|
47
|
+
if (!task)
|
|
48
|
+
throw new CommerceError("external_task", "Original commerce Task missing");
|
|
49
|
+
return task;
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
context(order) {
|
|
53
|
+
return new ServerCallContext({
|
|
54
|
+
user: { isAuthenticated: true, userName: order.caller },
|
|
55
|
+
tenant: order.serviceRevision,
|
|
56
|
+
requestedVersion: "1.0",
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
list() {
|
|
60
|
+
return this.store.externalTasks(this.principal, this.services).map((v) => ({
|
|
61
|
+
taskId: v.order.taskId,
|
|
62
|
+
serviceId: v.order.serviceRevision.split(":")[0],
|
|
63
|
+
claimId: v.claimId,
|
|
64
|
+
state: v.order.executionState,
|
|
65
|
+
input: v.order.input,
|
|
66
|
+
paymentState: v.order.paymentState,
|
|
67
|
+
}));
|
|
68
|
+
}
|
|
69
|
+
async claim(taskId) {
|
|
70
|
+
const value = this.store.claimExternalTask(taskId, this.principal, this.services);
|
|
71
|
+
const order = value.order;
|
|
72
|
+
const original = await this.store.load(taskId, this.context(order));
|
|
73
|
+
if (!original)
|
|
74
|
+
throw new CommerceError("external_task", "Original Task missing");
|
|
75
|
+
if (![TaskState.TASK_STATE_COMPLETED, TaskState.TASK_STATE_FAILED].includes(original.status?.state ?? 0))
|
|
76
|
+
await this.store.save(Task.fromJSON({
|
|
77
|
+
...Task.toJSON(original),
|
|
78
|
+
status: { state: "TASK_STATE_WORKING" },
|
|
79
|
+
metadata: { ...original.metadata, externalAgent: this.principal },
|
|
80
|
+
}), this.context(order));
|
|
81
|
+
return {
|
|
82
|
+
taskId,
|
|
83
|
+
claimId: value.claimId,
|
|
84
|
+
input: order.input,
|
|
85
|
+
paymentState: order.paymentState,
|
|
86
|
+
state: order.executionState,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
async complete(taskId, claimId, text, failed = false) {
|
|
90
|
+
if (typeof text !== "string" ||
|
|
91
|
+
!text.trim() ||
|
|
92
|
+
Buffer.byteLength(text) > 32000)
|
|
93
|
+
throw new CommerceError("external_result", "Return one bounded nonempty result");
|
|
94
|
+
const order = this.store.externalClaim(taskId, this.principal, claimId, this.services);
|
|
95
|
+
const original = await this.store.load(taskId, this.context(order));
|
|
96
|
+
if (!original)
|
|
97
|
+
throw new CommerceError("external_task", "Original Task missing");
|
|
98
|
+
const state = failed
|
|
99
|
+
? TaskState.TASK_STATE_FAILED
|
|
100
|
+
: TaskState.TASK_STATE_COMPLETED;
|
|
101
|
+
if ([TaskState.TASK_STATE_COMPLETED, TaskState.TASK_STATE_FAILED].includes(original.status?.state ?? 0)) {
|
|
102
|
+
const previous = original.artifacts
|
|
103
|
+
.flatMap((a) => a.parts)
|
|
104
|
+
.map((p) => (p.content?.$case === "text" ? p.content.value : ""))
|
|
105
|
+
.join("\n");
|
|
106
|
+
if (original.status?.state !== state || previous !== text)
|
|
107
|
+
throw new CommerceError("external_result_conflict", "A completed original result cannot be replaced");
|
|
108
|
+
return Task.toJSON(original);
|
|
109
|
+
}
|
|
110
|
+
const result = Task.fromJSON({
|
|
111
|
+
...Task.toJSON(original),
|
|
112
|
+
status: { state: failed ? "TASK_STATE_FAILED" : "TASK_STATE_COMPLETED" },
|
|
113
|
+
artifacts: [
|
|
114
|
+
{
|
|
115
|
+
artifactId: "external-result",
|
|
116
|
+
name: "external-agent-result.txt",
|
|
117
|
+
parts: [{ text, mediaType: "text/plain" }],
|
|
118
|
+
},
|
|
119
|
+
],
|
|
120
|
+
metadata: {
|
|
121
|
+
...original.metadata,
|
|
122
|
+
externalAgent: this.principal,
|
|
123
|
+
claimId,
|
|
124
|
+
},
|
|
125
|
+
});
|
|
126
|
+
await this.store.save(result, this.context(order), original);
|
|
127
|
+
return Task.toJSON(result);
|
|
128
|
+
}
|
|
129
|
+
}
|