@envarai/envarpay 0.2.0-alpha.2 → 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 -56
- package/dist/commerce/agent-setup.d.ts +19 -0
- package/dist/commerce/agent-setup.js +210 -0
- package/dist/commerce/alerts.d.ts +27 -0
- package/dist/commerce/alerts.js +71 -0
- package/dist/commerce/card.js +3 -3
- package/dist/commerce/cli.js +91 -119
- package/dist/commerce/config.d.ts +3 -2
- package/dist/commerce/config.js +19 -77
- 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 +3 -1
- package/dist/commerce/envar-runtime.js +28 -1
- 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/index.d.ts +1 -0
- package/dist/commerce/index.js +1 -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 +32 -0
- package/dist/commerce/native-skills.js +174 -0
- package/dist/commerce/openclaw-setup.d.ts +28 -0
- package/dist/commerce/openclaw-setup.js +43 -0
- package/dist/commerce/operations.d.ts +8 -3
- package/dist/commerce/operations.js +21 -18
- package/dist/commerce/pricing.d.ts +0 -1
- package/dist/commerce/pricing.js +3 -5
- package/dist/commerce/runtime.d.ts +4 -2
- package/dist/commerce/runtime.js +4 -2
- package/dist/commerce/server.d.ts +3 -2
- package/dist/commerce/server.js +26 -10
- package/dist/commerce/setup.d.ts +25 -0
- package/dist/commerce/setup.js +379 -0
- package/dist/commerce/skill-gate.d.ts +54 -0
- package/dist/commerce/skill-gate.js +153 -0
- package/dist/commerce/store.d.ts +11 -1
- package/dist/commerce/store.js +36 -2
- package/dist/commerce/types.d.ts +6 -57
- package/examples/envar-integration-policy.json +1 -9
- package/examples/hermes-setup-probe.py +109 -0
- package/examples/hermes-skill-inventory.py +108 -0
- package/examples/native-skill-runner.py +197 -0
- package/examples/seller.json +143 -26
- package/package.json +5 -16
- package/schemas/envarpay.config.schema.json +28 -87
- package/dist/commerce/buyer-store.d.ts +0 -101
- package/dist/commerce/buyer-store.js +0 -168
- package/dist/commerce/buyer.d.ts +0 -132
- package/dist/commerce/buyer.js +0 -755
- 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 -138
- package/dist/commerce/mpp-client.d.ts +0 -59
- package/dist/commerce/mpp-client.js +0 -24
- package/dist/commerce/mpp-runtime.d.ts +0 -3
- package/dist/commerce/mpp-runtime.js +0 -3
- 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-provider.d.ts +0 -9
- package/dist/commerce/stripe-provider.js +0 -97
- package/dist/commerce/x402.d.ts +0 -37
- package/dist/commerce/x402.js +0 -174
- package/examples/buyer-policy.json +0 -18
- package/schemas/buyer-policy.schema.json +0 -253
|
@@ -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("../../examples/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
|
+
}
|
|
@@ -8,10 +8,11 @@ export declare class PinnedUpstreams {
|
|
|
8
8
|
private readonly inputEncoding;
|
|
9
9
|
private readonly fetchImpl;
|
|
10
10
|
private readonly signal?;
|
|
11
|
+
private readonly skills?;
|
|
11
12
|
private bindings;
|
|
12
13
|
private readonly executors;
|
|
13
14
|
readonly execute: ExecuteOrder;
|
|
14
|
-
constructor(path: string, credentials: Readonly<Record<string, string>>, inputEncoding?: Readonly<Record<string, "data" | "json-text">>, fetchImpl?: typeof fetch, signal?: AbortSignal | undefined);
|
|
15
|
+
constructor(path: string, credentials: Readonly<Record<string, string>>, inputEncoding?: Readonly<Record<string, "data" | "json-text">>, fetchImpl?: typeof fetch, signal?: AbortSignal | undefined, skills?: ExecuteOrder | undefined);
|
|
15
16
|
prepare(config: CommerceConfig, history?: readonly Service[]): void;
|
|
16
17
|
private executor;
|
|
17
18
|
}
|
|
@@ -30,6 +31,7 @@ export interface EnvarSellerRuntimeOptions {
|
|
|
30
31
|
/** Local payment adapters/merchant credentials must already exist; config sync cannot create them. */
|
|
31
32
|
validateConfig?: (config: CommerceConfig) => void;
|
|
32
33
|
onError?: (code: string) => void;
|
|
34
|
+
beforeSync?: () => Promise<void>;
|
|
33
35
|
}
|
|
34
36
|
export declare class EnvarSellerRuntime {
|
|
35
37
|
private readonly options;
|
|
@@ -50,15 +50,17 @@ export class PinnedUpstreams {
|
|
|
50
50
|
inputEncoding;
|
|
51
51
|
fetchImpl;
|
|
52
52
|
signal;
|
|
53
|
+
skills;
|
|
53
54
|
bindings = {};
|
|
54
55
|
executors = new Map();
|
|
55
56
|
execute;
|
|
56
|
-
constructor(path, credentials, inputEncoding = {}, fetchImpl = fetch, signal) {
|
|
57
|
+
constructor(path, credentials, inputEncoding = {}, fetchImpl = fetch, signal, skills) {
|
|
57
58
|
this.path = path;
|
|
58
59
|
this.credentials = credentials;
|
|
59
60
|
this.inputEncoding = inputEncoding;
|
|
60
61
|
this.fetchImpl = fetchImpl;
|
|
61
62
|
this.signal = signal;
|
|
63
|
+
this.skills = skills;
|
|
62
64
|
if (existsSync(path)) {
|
|
63
65
|
const value = privateJson(path);
|
|
64
66
|
if (value.version !== 1 ||
|
|
@@ -92,10 +94,22 @@ export class PinnedUpstreams {
|
|
|
92
94
|
}
|
|
93
95
|
return undefined;
|
|
94
96
|
};
|
|
97
|
+
if (skills) {
|
|
98
|
+
this.execute.assertSkill = (service) => skills.assertSkill(service);
|
|
99
|
+
this.execute.skillCard = skills.skillCard;
|
|
100
|
+
}
|
|
95
101
|
}
|
|
96
102
|
prepare(config, history = []) {
|
|
97
103
|
const next = structuredClone(this.bindings), current = new Set(config.services.map((s) => `${s.id}:${s.revision}`));
|
|
98
104
|
for (const service of [...history, ...config.services]) {
|
|
105
|
+
if (service.execution.type === 'skill') {
|
|
106
|
+
if (!this.skills)
|
|
107
|
+
throw new CommerceError('skill_gate_required', 'Configure the native skill runtime');
|
|
108
|
+
// Retired versions stay readable even after their skill package is upgraded.
|
|
109
|
+
if (current.has(`${service.id}:${service.revision}`))
|
|
110
|
+
this.skills.assertSkill(service);
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
99
113
|
const key = `${service.id}:${service.revision}`, saved = next[key], explicit = this.credentials[key];
|
|
100
114
|
if (saved) {
|
|
101
115
|
if (saved.cardUrl !== service.execution.cardUrl ||
|
|
@@ -120,6 +134,11 @@ export class PinnedUpstreams {
|
|
|
120
134
|
this.bindings = next;
|
|
121
135
|
}
|
|
122
136
|
executor(service) {
|
|
137
|
+
if (service.execution.type === 'skill') {
|
|
138
|
+
if (!this.skills)
|
|
139
|
+
throw new CommerceError('skill_gate_required', 'Configure the native skill runtime');
|
|
140
|
+
return this.skills;
|
|
141
|
+
}
|
|
123
142
|
const key = `${service.id}:${service.revision}`, binding = this.bindings[key];
|
|
124
143
|
if (!binding || binding.cardUrl !== service.execution.cardUrl)
|
|
125
144
|
throw new CommerceError("upstream_binding_missing", "Original runtime credential binding is unavailable");
|
|
@@ -147,6 +166,7 @@ export function mergeEnvarService(base, candidate) {
|
|
|
147
166
|
const profiles = { ...base.paymentProfiles, ...incoming.paymentProfiles }, services = [...retained, service], used = new Set(services.flatMap((s) => s.offers.map((o) => o.paymentProfile).filter((p) => !!p)));
|
|
148
167
|
return loadCommerceConfig({
|
|
149
168
|
...base,
|
|
169
|
+
configVersion: Math.max(base.configVersion, incoming.configVersion),
|
|
150
170
|
paymentProfiles: Object.fromEntries(Object.entries(profiles).filter(([key]) => used.has(key))),
|
|
151
171
|
services,
|
|
152
172
|
});
|
|
@@ -211,6 +231,13 @@ export class EnvarSellerRuntime {
|
|
|
211
231
|
return this.running;
|
|
212
232
|
this.running = (async () => {
|
|
213
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
|
+
}
|
|
214
241
|
for (const candidate of await this.options.integration.pullCandidates())
|
|
215
242
|
await this.options.integration.applyCandidate(candidate, (c) => this.apply(c));
|
|
216
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: "working" | "input_required" | "completed" | "failed" | "unknown" | "not_started" | "queued" | "dispatching" | "auth_required";
|
|
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: "working" | "input_required" | "completed" | "failed" | "unknown" | "not_started" | "queued" | "dispatching" | "auth_required";
|
|
24
|
+
}>;
|
|
25
|
+
complete(taskId: string, claimId: string, text: string, failed?: boolean): Promise<unknown>;
|
|
26
|
+
}
|