@envarai/envarpay 0.1.0-alpha.3 → 0.2.0-alpha.10
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 +57 -145
- package/dist/commerce/alerts.d.ts +37 -0
- package/dist/commerce/alerts.js +127 -0
- package/dist/commerce/buyer-store.d.ts +101 -0
- package/dist/commerce/buyer-store.js +168 -0
- package/dist/commerce/buyer.d.ts +138 -0
- package/dist/commerce/buyer.js +775 -0
- package/dist/commerce/card.d.ts +5 -0
- package/dist/commerce/card.js +29 -0
- package/dist/commerce/chain.d.ts +24 -0
- package/dist/commerce/chain.js +146 -0
- package/dist/commerce/cli.d.ts +2 -0
- package/dist/commerce/cli.js +318 -0
- package/dist/commerce/client-runtime.d.ts +8 -0
- package/dist/commerce/client-runtime.js +7 -0
- package/dist/commerce/config.d.ts +11 -0
- package/dist/commerce/config.js +200 -0
- package/dist/commerce/envar-runtime.d.ts +47 -0
- package/dist/commerce/envar-runtime.js +276 -0
- package/dist/commerce/envar.d.ts +126 -0
- package/dist/commerce/envar.js +647 -0
- package/dist/commerce/http.d.ts +2 -0
- package/dist/commerce/http.js +34 -0
- package/dist/commerce/index.d.ts +5 -0
- package/dist/commerce/index.js +5 -0
- package/dist/commerce/management.d.ts +14 -0
- package/dist/commerce/management.js +140 -0
- package/dist/commerce/mpp-client.d.ts +70 -0
- package/dist/commerce/mpp-client.js +29 -0
- package/dist/commerce/mpp-runtime.d.ts +4 -0
- package/dist/commerce/mpp-runtime.js +4 -0
- package/dist/commerce/mpp.d.ts +53 -0
- package/dist/commerce/mpp.js +163 -0
- package/dist/commerce/native-skills.d.ts +27 -0
- package/dist/commerce/native-skills.js +176 -0
- package/dist/commerce/operations.d.ts +22 -0
- package/dist/commerce/operations.js +39 -0
- package/dist/commerce/peer-proxy.d.ts +44 -0
- package/dist/commerce/peer-proxy.js +673 -0
- package/dist/commerce/pricing.d.ts +12 -0
- package/dist/commerce/pricing.js +60 -0
- package/dist/commerce/runtime.d.ts +10 -0
- package/dist/commerce/runtime.js +10 -0
- package/dist/commerce/server.d.ts +61 -0
- package/dist/commerce/server.js +516 -0
- package/dist/commerce/setup.d.ts +18 -0
- package/dist/commerce/setup.js +344 -0
- package/dist/commerce/skill-gate.d.ts +54 -0
- package/dist/commerce/skill-gate.js +153 -0
- package/dist/commerce/store.d.ts +88 -0
- package/dist/commerce/store.js +473 -0
- package/dist/commerce/stripe-issuer.d.ts +41 -0
- package/dist/commerce/stripe-issuer.js +351 -0
- package/dist/commerce/stripe-provider.d.ts +11 -0
- package/dist/commerce/stripe-provider.js +132 -0
- package/dist/commerce/types.d.ts +138 -0
- package/dist/commerce/types.js +8 -0
- package/dist/commerce/upstream.d.ts +6 -0
- package/dist/commerce/upstream.js +103 -0
- package/dist/commerce/vault.d.ts +8 -0
- package/dist/commerce/vault.js +55 -0
- package/dist/commerce/x402.d.ts +38 -0
- package/dist/commerce/x402.js +181 -0
- package/examples/buyer-policy.json +18 -0
- package/examples/envar-integration-policy.json +18 -0
- package/examples/hermes-setup-probe.py +105 -0
- package/examples/native-skill-runner.py +197 -0
- package/examples/seller.json +52 -0
- package/package.json +75 -10
- package/schemas/buyer-policy.schema.json +253 -0
- package/schemas/envarpay.config.schema.json +383 -0
- package/schemas/quote.schema.json +91 -0
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/** Owner-operated, instruction-only skill runtime. No model file, shell or network tools. */
|
|
2
|
+
import { AgentCard, Task } from '@a2a-js/sdk';
|
|
3
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
4
|
+
import { lstatSync, readFileSync, readdirSync, mkdirSync } from 'node:fs';
|
|
5
|
+
import { resolve, join } from 'node:path';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
import { spawn } from 'node:child_process';
|
|
8
|
+
import { DatabaseSync } from 'node:sqlite';
|
|
9
|
+
import { digest } from './config.js';
|
|
10
|
+
import { CommerceError } from './types.js';
|
|
11
|
+
import { skillScopedExecutor } from './skill-gate.js';
|
|
12
|
+
const namePattern = /^[a-z][a-z0-9-]{0,63}$/;
|
|
13
|
+
/** Hash every resource; reject symlinks, unbounded packages and executable skill requirements. */
|
|
14
|
+
export function readInstalledSkills(root, freeSkills, cardUrl) {
|
|
15
|
+
const base = resolve(root), rootStat = lstatSync(base);
|
|
16
|
+
if (!rootStat.isDirectory() || rootStat.isSymbolicLink())
|
|
17
|
+
throw new CommerceError('skill_directory', 'Use a real installed skill directory');
|
|
18
|
+
const output = [];
|
|
19
|
+
for (const entry of readdirSync(base, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
20
|
+
if (entry.name.startsWith('.'))
|
|
21
|
+
continue;
|
|
22
|
+
if (!entry.isDirectory() || entry.isSymbolicLink() || !namePattern.test(entry.name))
|
|
23
|
+
throw new CommerceError('skill_package', 'Use one real directory per exact skill name');
|
|
24
|
+
const files = [];
|
|
25
|
+
let total = 0;
|
|
26
|
+
function walk(folder, prefix = '') {
|
|
27
|
+
if (prefix.split('/').length > 12)
|
|
28
|
+
throw new CommerceError('skill_size', 'Skill package nesting exceeds the supported limit');
|
|
29
|
+
for (const item of readdirSync(folder, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
30
|
+
const path = join(folder, item.name), relative = prefix + item.name;
|
|
31
|
+
if (item.isSymbolicLink())
|
|
32
|
+
throw new CommerceError('skill_symlink', 'Skill packages cannot contain symlinks');
|
|
33
|
+
if (item.isDirectory()) {
|
|
34
|
+
walk(path, relative + '/');
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
const stat = lstatSync(path);
|
|
38
|
+
if (!stat.isFile() || stat.size > 512 * 1024 || files.length >= 128 || (total += stat.size) > 1024 * 1024)
|
|
39
|
+
throw new CommerceError('skill_size', 'Skill package exceeds supported size');
|
|
40
|
+
files.push({ path: relative, sha256: createHash('sha256').update(readFileSync(path)).digest('hex') });
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
walk(join(base, entry.name));
|
|
44
|
+
const instructions = readFileSync(join(base, entry.name, 'SKILL.md'), 'utf8');
|
|
45
|
+
const front = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/.exec(instructions)?.[1];
|
|
46
|
+
const name = /^name:\s*["']?([a-z][a-z0-9-]*)["']?\s*$/m.exec(front ?? '')?.[1];
|
|
47
|
+
if (name !== entry.name || instructions.length > 48000)
|
|
48
|
+
throw new CommerceError('skill_name_mismatch', 'SKILL.md name must match its installed directory');
|
|
49
|
+
// This adapter deliberately supports only text-in/text-out workflows. Owners must use
|
|
50
|
+
// a separately reviewed adapter for skills needing code, browsing, external APIs or files.
|
|
51
|
+
if (!/^envar-runtime:\s*instruction-only\s*$/m.test(front ?? ''))
|
|
52
|
+
throw new CommerceError('skill_runtime_required', 'Review and mark each compatible skill envar-runtime: instruction-only before publication');
|
|
53
|
+
const description = /^description:\s*(.+)$/m.exec(front ?? '')?.[1]?.replace(/^["']|["']$/g, '') ?? name;
|
|
54
|
+
const references = files.filter(file => file.path !== 'SKILL.md' && file.path.endsWith('.md')).map(file => `\n\n<skill-reference path="${file.path}">\n${readFileSync(join(base, entry.name, file.path), 'utf8')}\n</skill-reference>`).join('');
|
|
55
|
+
if (instructions.length + references.length > 120000)
|
|
56
|
+
throw new CommerceError('skill_size', 'Instruction resources exceed the supported context size');
|
|
57
|
+
output.push({ descriptor: { name, digest: digest(files), cardUrl, access: freeSkills.includes(name) ? 'free' : 'paid', description }, instructions: instructions + references });
|
|
58
|
+
}
|
|
59
|
+
if (!output.length || output.length > 32 || freeSkills.some(name => !output.some(s => s.descriptor.name === name)))
|
|
60
|
+
throw new CommerceError('skill_inventory', 'Install between 1 and 32 skills and declare only installed free skills');
|
|
61
|
+
// Free helpers are executable dependencies of every purchase, so their versions
|
|
62
|
+
// and access-policy changes must invalidate the quote's selected skill digest too.
|
|
63
|
+
const helpers = output.filter(s => s.descriptor.access === 'free').map(s => ({ name: s.descriptor.name, digest: s.descriptor.digest }));
|
|
64
|
+
for (const skill of output)
|
|
65
|
+
skill.descriptor.digest = digest({ package: skill.descriptor.digest, helpers });
|
|
66
|
+
return output;
|
|
67
|
+
}
|
|
68
|
+
export function createNativeSkillExecutor(settings, origin, agentName) {
|
|
69
|
+
if (!['hermes', 'openclaw'].includes(settings.framework) || !Array.isArray(settings.freeSkills) || !settings.python || !settings.model || !settings.baseUrl)
|
|
70
|
+
throw new CommerceError('skill_settings', 'Configure a supported native skill runtime');
|
|
71
|
+
const cardUrl = new URL('/.well-known/agent-card.json', origin).href;
|
|
72
|
+
const installed = () => readInstalledSkills(settings.skillsDirectory, settings.freeSkills, cardUrl);
|
|
73
|
+
const initial = installed();
|
|
74
|
+
mkdirSync(settings.stateDirectory, { recursive: true, mode: 0o700 });
|
|
75
|
+
const stateStat = lstatSync(settings.stateDirectory);
|
|
76
|
+
if (!stateStat.isDirectory() || stateStat.isSymbolicLink() || (stateStat.mode & 0o077) !== 0)
|
|
77
|
+
throw new CommerceError('skill_state', 'Skill state must be owner-only');
|
|
78
|
+
const db = new DatabaseSync(join(settings.stateDirectory, 'skills.sqlite3'));
|
|
79
|
+
db.exec('PRAGMA journal_mode=WAL; PRAGMA synchronous=FULL; CREATE TABLE IF NOT EXISTS skill_tasks (id TEXT PRIMARY KEY, order_id TEXT UNIQUE NOT NULL, binding TEXT NOT NULL, task_json TEXT NOT NULL)');
|
|
80
|
+
for (const row of db.prepare('SELECT id,task_json FROM skill_tasks').all()) {
|
|
81
|
+
const task = JSON.parse(String(row.task_json));
|
|
82
|
+
if (task.status?.state === 'TASK_STATE_WORKING') {
|
|
83
|
+
task.status = { state: 'TASK_STATE_FAILED', message: { role: 'ROLE_AGENT', messageId: randomUUID(), parts: [{ text: 'Native execution interrupted; inspect this original order' }] } };
|
|
84
|
+
db.prepare('UPDATE skill_tasks SET task_json=? WHERE id=?').run(JSON.stringify(task), row.id);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
const inFlight = new Map();
|
|
88
|
+
const runner = fileURLToPath(new URL('../../examples/native-skill-runner.py', import.meta.url));
|
|
89
|
+
function readTask(id, invocation) {
|
|
90
|
+
const row = db.prepare('SELECT order_id,task_json FROM skill_tasks WHERE id=?').get(id);
|
|
91
|
+
if (!row || row.order_id !== invocation.orderId)
|
|
92
|
+
throw new CommerceError('skill_task_not_found', 'Original skill Task not found');
|
|
93
|
+
return Task.fromJSON(JSON.parse(String(row.task_json)));
|
|
94
|
+
}
|
|
95
|
+
function executeProcess(payload) {
|
|
96
|
+
return new Promise((accept, reject) => {
|
|
97
|
+
const docker = settings.dockerContainer;
|
|
98
|
+
if (docker && !/^[a-zA-Z0-9][a-zA-Z0-9_.-]{0,127}$/.test(docker))
|
|
99
|
+
throw new CommerceError('skill_container', 'Invalid Docker container name');
|
|
100
|
+
const child = docker
|
|
101
|
+
? spawn('docker', ['exec', '-i', docker, settings.python, '-c', readFileSync(runner, 'utf8')], { stdio: ['pipe', 'pipe', 'pipe'], env: { PATH: process.env.PATH, HOME: process.env.HOME, DOCKER_CONFIG: process.env.DOCKER_CONFIG, DOCKER_HOST: process.env.DOCKER_HOST } })
|
|
102
|
+
: spawn(settings.python, [runner], { stdio: ['pipe', 'pipe', 'pipe'], env: { PATH: process.env.PATH, LANG: 'C.UTF-8', PYTHONUNBUFFERED: '1' } });
|
|
103
|
+
const chunks = [];
|
|
104
|
+
let size = 0;
|
|
105
|
+
const timeout = setTimeout(() => child.kill('SIGTERM'), 240000);
|
|
106
|
+
child.stdout.on('data', (chunk) => { size += chunk.length; if (size > 1024 * 1024)
|
|
107
|
+
child.kill('SIGTERM');
|
|
108
|
+
else
|
|
109
|
+
chunks.push(chunk); });
|
|
110
|
+
child.stderr.resume();
|
|
111
|
+
child.on('error', () => { clearTimeout(timeout); reject(new CommerceError('skill_runtime_start', 'Native skill runtime could not start')); });
|
|
112
|
+
child.on('close', code => {
|
|
113
|
+
clearTimeout(timeout);
|
|
114
|
+
try {
|
|
115
|
+
if (code !== 0 || size > 1024 * 1024)
|
|
116
|
+
throw Error();
|
|
117
|
+
const result = JSON.parse(Buffer.concat(chunks).toString('utf8'));
|
|
118
|
+
if (typeof result.output !== 'string' || !result.output.trim() || result.framework !== settings.framework || !Array.isArray(result.tools) || result.tools.length)
|
|
119
|
+
throw Error();
|
|
120
|
+
accept(result);
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
reject(new CommerceError('skill_runtime_failed', 'Native skill execution failed; inspect the original Task without resubmitting'));
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
child.stdin.on('error', () => { });
|
|
127
|
+
child.stdin.end(JSON.stringify(payload));
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
const runtime = {
|
|
131
|
+
list: () => installed().map(s => s.descriptor),
|
|
132
|
+
async send(name, input, invocation) {
|
|
133
|
+
const skills = installed(), selected = skills.find(s => s.descriptor.name === name);
|
|
134
|
+
if (!selected || selected.descriptor.digest !== invocation.grant.skillDigest)
|
|
135
|
+
throw new CommerceError('skill_changed', 'The purchased skill version is no longer installed');
|
|
136
|
+
invocation.grant.require(name);
|
|
137
|
+
const allowed = skills.filter(s => s.descriptor.name === name || s.descriptor.access === 'free');
|
|
138
|
+
allowed.forEach(s => invocation.grant.require(s.descriptor.name));
|
|
139
|
+
const binding = digest({ name, version: selected.descriptor.digest, caller: invocation.grant.caller, input, allowed: allowed.map(s => s.descriptor) });
|
|
140
|
+
const prior = db.prepare('SELECT id,binding FROM skill_tasks WHERE order_id=?').get(invocation.orderId);
|
|
141
|
+
if (prior) {
|
|
142
|
+
if (prior.binding !== binding)
|
|
143
|
+
throw new CommerceError('skill_task_conflict', 'An order already identifies a different skill input');
|
|
144
|
+
return readTask(String(prior.id), invocation);
|
|
145
|
+
}
|
|
146
|
+
const id = randomUUID();
|
|
147
|
+
// A crash cannot cause a second native dispatch. The durable Task remains failed until owner inspection.
|
|
148
|
+
const pending = Task.fromJSON({ id, status: { state: 'TASK_STATE_WORKING' } });
|
|
149
|
+
db.prepare('INSERT INTO skill_tasks VALUES (?,?,?,?)').run(id, invocation.orderId, binding, JSON.stringify(Task.toJSON(pending)));
|
|
150
|
+
const operation = (async () => {
|
|
151
|
+
let task;
|
|
152
|
+
try {
|
|
153
|
+
const result = await executeProcess({ settings: { ...settings, stateDirectory: join(settings.dockerStateDirectory ?? settings.stateDirectory, id) }, skillName: name, skills: allowed.map(s => ({ name: s.descriptor.name, instructions: s.instructions })), input });
|
|
154
|
+
task = Task.fromJSON({ id, status: { state: 'TASK_STATE_COMPLETED' }, artifacts: [{ artifactId: 'result', name: name + '.md', parts: [{ text: result.output, mediaType: 'text/markdown' }] }], metadata: { skillName: name, skillDigest: selected.descriptor.digest, allowedSkills: allowed.map(s => s.descriptor.name), framework: settings.framework, tools: result.tools, sessionId: result.sessionId ?? id } });
|
|
155
|
+
}
|
|
156
|
+
catch (error) {
|
|
157
|
+
task = Task.fromJSON({ id, status: { state: 'TASK_STATE_FAILED' }, metadata: { errorCode: error instanceof CommerceError ? error.code : 'skill_runtime_failed' } });
|
|
158
|
+
}
|
|
159
|
+
db.prepare('UPDATE skill_tasks SET task_json=? WHERE id=?').run(JSON.stringify(Task.toJSON(task)), id);
|
|
160
|
+
return task;
|
|
161
|
+
})();
|
|
162
|
+
inFlight.set(id, operation);
|
|
163
|
+
void operation.finally(() => inFlight.delete(id));
|
|
164
|
+
return pending;
|
|
165
|
+
},
|
|
166
|
+
async getTask(_name, id, invocation) { return readTask(id, invocation); },
|
|
167
|
+
};
|
|
168
|
+
const execute = skillScopedExecutor(runtime);
|
|
169
|
+
execute.skillCard = () => {
|
|
170
|
+
const skills = installed();
|
|
171
|
+
return AgentCard.fromJSON({ name: agentName, description: 'Installed Agent Skills', version: '1.0', supportedInterfaces: [{ url: new URL('/a2a', origin).href, protocolBinding: 'JSONRPC', protocolVersion: '1.0' }], capabilities: { extensions: [{ uri: 'urn:envarpay:skill-gate:1', required: false, params: { runtime: 'instruction-only-v1', skills: skills.map(s => ({ name: s.descriptor.name, digest: s.descriptor.digest, access: s.descriptor.access })) } }] }, defaultInputModes: ['application/json'], defaultOutputModes: ['text/markdown'], skills: skills.map(s => ({ id: s.descriptor.name, name: s.descriptor.name, description: s.descriptor.description, tags: [s.descriptor.name] })) });
|
|
172
|
+
};
|
|
173
|
+
if (initial.length === 0)
|
|
174
|
+
throw new CommerceError('skill_inventory', 'No installed skills');
|
|
175
|
+
return { execute, close: async () => { await Promise.allSettled(inFlight.values()); db.close(); } };
|
|
176
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export { inspectLocalAlerts, type LocalAlertOptions } from './alerts.js';
|
|
2
|
+
export type InspectionView = 'attention' | 'refund-review';
|
|
3
|
+
/** Local owner-only diagnostics: never opens a signer, vault, provider or network. */
|
|
4
|
+
export declare function inspectLedger(path: string, after?: number, limit?: number, view?: InspectionView): {
|
|
5
|
+
role: string;
|
|
6
|
+
view: InspectionView;
|
|
7
|
+
total: number;
|
|
8
|
+
attentionCount: number;
|
|
9
|
+
refundReviewCount: number;
|
|
10
|
+
items: {
|
|
11
|
+
reason?: "confirmed_payment_failed_execution" | undefined;
|
|
12
|
+
cursor: number;
|
|
13
|
+
id: string;
|
|
14
|
+
paymentState: string;
|
|
15
|
+
executionState: string;
|
|
16
|
+
taskId: string | null;
|
|
17
|
+
}[];
|
|
18
|
+
nextCursor: number;
|
|
19
|
+
hasMore: boolean;
|
|
20
|
+
paymentPerformed: boolean;
|
|
21
|
+
refundPerformed: boolean;
|
|
22
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { DatabaseSync } from 'node:sqlite';
|
|
2
|
+
import { lstatSync } from 'node:fs';
|
|
3
|
+
import { CommerceError } from './types.js';
|
|
4
|
+
export { inspectLocalAlerts } from './alerts.js';
|
|
5
|
+
/** Local owner-only diagnostics: never opens a signer, vault, provider or network. */
|
|
6
|
+
export function inspectLedger(path, after = 0, limit = 100, view = 'attention') {
|
|
7
|
+
const stat = lstatSync(path);
|
|
8
|
+
if (!stat.isFile() || stat.isSymbolicLink() || (stat.mode & 0o077) !== 0 || (process.getuid && stat.uid !== process.getuid()))
|
|
9
|
+
throw new CommerceError('private_file', 'Inspect an existing ledger owned only by the current user');
|
|
10
|
+
if (!Number.isSafeInteger(after) || after < 0 || !Number.isSafeInteger(limit) || limit < 1 || limit > 100)
|
|
11
|
+
throw new CommerceError('inspection_cursor', 'Use a nonnegative row cursor and a limit from1 to100');
|
|
12
|
+
if (!['attention', 'refund-review'].includes(view))
|
|
13
|
+
throw new CommerceError('inspection_view', 'Use attention or refund-review inspection');
|
|
14
|
+
const db = new DatabaseSync(path, { readOnly: true });
|
|
15
|
+
try {
|
|
16
|
+
db.exec('PRAGMA query_only=ON; PRAGMA busy_timeout=1000');
|
|
17
|
+
const names = new Set(db.prepare("SELECT name FROM sqlite_master WHERE type='table'").all().map(row => row.name));
|
|
18
|
+
const role = names.has('buyer_purchases') ? 'buyer' : names.has('commerce_orders') ? 'seller' : null;
|
|
19
|
+
if (!role)
|
|
20
|
+
throw new CommerceError('store_version', 'Unrecognized commerce ledger');
|
|
21
|
+
const meta = role === 'buyer' ? 'buyer_meta' : 'commerce_meta', version = role === 'buyer' ? 1 : 2;
|
|
22
|
+
if (db.prepare(`SELECT version FROM ${meta}`).get()?.version !== version)
|
|
23
|
+
throw new CommerceError('store_version', 'Unsupported commerce ledger version');
|
|
24
|
+
const table = role === 'buyer' ? 'buyer_purchases' : 'commerce_orders';
|
|
25
|
+
const total = Number(db.prepare(`SELECT COUNT(*) AS n FROM ${table}`).get().n);
|
|
26
|
+
const attention = role === 'buyer' ? "json_extract(record_json,'$.state') IN ('signing','submitted','unknown') OR json_extract(record_json,'$.paymentState') IN ('reserved','unknown') OR json_extract(record_json,'$.executionState')='unknown'" : "payment_state IN ('settling','unknown') OR execution_state IN ('dispatching','unknown')";
|
|
27
|
+
const refundReview = role === 'buyer' ? "json_extract(record_json,'$.paymentState')='confirmed' AND json_extract(record_json,'$.executionState')='failed'" : "payment_state='confirmed' AND execution_state='failed'";
|
|
28
|
+
const filter = view === 'refund-review' ? refundReview : attention;
|
|
29
|
+
const selected = role === 'buyer' ? "rowid AS cursor,id,json_extract(record_json,'$.paymentState') AS payment_state,json_extract(record_json,'$.executionState') AS execution_state,json_extract(record_json,'$.task.id') AS task_id" : "rowid AS cursor,id,payment_state,execution_state,task_id";
|
|
30
|
+
const rows = db.prepare(`SELECT ${selected} FROM ${table} WHERE rowid>? AND (${filter}) ORDER BY rowid LIMIT ?`).all(after, limit + 1);
|
|
31
|
+
const page = rows.slice(0, limit).map(row => ({ cursor: Number(row.cursor), id: String(row.id), paymentState: String(row.payment_state), executionState: String(row.execution_state), taskId: typeof row.task_id === 'string' ? row.task_id : null, ...(view === 'refund-review' ? { reason: 'confirmed_payment_failed_execution' } : {}) }));
|
|
32
|
+
const attentionCount = Number(db.prepare(`SELECT COUNT(*) AS n FROM ${table} WHERE ${attention}`).get().n);
|
|
33
|
+
const refundReviewCount = Number(db.prepare(`SELECT COUNT(*) AS n FROM ${table} WHERE ${refundReview}`).get().n);
|
|
34
|
+
return { role, view, total, attentionCount, refundReviewCount, items: page, nextCursor: page.at(-1)?.cursor ?? after, hasMore: rows.length > limit, paymentPerformed: false, refundPerformed: false };
|
|
35
|
+
}
|
|
36
|
+
finally {
|
|
37
|
+
db.close();
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { AgentCard, Task, type SendMessageRequest } from "@a2a-js/sdk";
|
|
2
|
+
import { CommerceBuyer } from "./buyer.js";
|
|
3
|
+
export interface PeerProxyOptions {
|
|
4
|
+
buyer: CommerceBuyer;
|
|
5
|
+
buyerCaller: string;
|
|
6
|
+
peerId: string;
|
|
7
|
+
offerId: string;
|
|
8
|
+
origin: string;
|
|
9
|
+
stateDirectory: string;
|
|
10
|
+
tokens: Readonly<Record<string, string>>;
|
|
11
|
+
label?: string;
|
|
12
|
+
waitMilliseconds?: number;
|
|
13
|
+
/** Explicit opt-in for an isolated Docker/private HTTP network; public deployments require HTTPS. */
|
|
14
|
+
allowPrivateHttp?: boolean;
|
|
15
|
+
}
|
|
16
|
+
/** The proxy token is never a wallet-management token. One proxy is bound to one approved peer/offer. */
|
|
17
|
+
export declare class A2APeerProxy {
|
|
18
|
+
private readonly options;
|
|
19
|
+
readonly origin: string;
|
|
20
|
+
readonly card: AgentCard;
|
|
21
|
+
private readonly db;
|
|
22
|
+
private readonly lock;
|
|
23
|
+
private readonly rpc;
|
|
24
|
+
private readonly authenticate;
|
|
25
|
+
private readonly cardUrl;
|
|
26
|
+
private readonly waitMs;
|
|
27
|
+
private readonly active;
|
|
28
|
+
private stopped;
|
|
29
|
+
private closed;
|
|
30
|
+
constructor(options: PeerProxyOptions);
|
|
31
|
+
private row;
|
|
32
|
+
private phase;
|
|
33
|
+
private projection;
|
|
34
|
+
private exclusive;
|
|
35
|
+
private snapshot;
|
|
36
|
+
private wait;
|
|
37
|
+
send(params: SendMessageRequest, caller: string): Promise<Task>;
|
|
38
|
+
read(id: string, caller: string): Promise<Task>;
|
|
39
|
+
handle(request: Request): Promise<Response>;
|
|
40
|
+
assertListenHost(host: string): void;
|
|
41
|
+
stop(): Promise<void>;
|
|
42
|
+
close(): void;
|
|
43
|
+
}
|
|
44
|
+
export declare function listenPeerProxy(proxy: A2APeerProxy, host?: string, port?: number): import("http").Server<typeof import("http").IncomingMessage, typeof import("http").ServerResponse>;
|