@envarai/envarpay 0.1.0-alpha.3 → 0.2.0-alpha.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.
Files changed (58) hide show
  1. package/README.md +56 -146
  2. package/dist/commerce/buyer-store.d.ts +93 -0
  3. package/dist/commerce/buyer-store.js +168 -0
  4. package/dist/commerce/buyer.d.ts +129 -0
  5. package/dist/commerce/buyer.js +631 -0
  6. package/dist/commerce/card.d.ts +5 -0
  7. package/dist/commerce/card.js +29 -0
  8. package/dist/commerce/chain.d.ts +24 -0
  9. package/dist/commerce/chain.js +146 -0
  10. package/dist/commerce/cli.d.ts +2 -0
  11. package/dist/commerce/cli.js +268 -0
  12. package/dist/commerce/client-runtime.d.ts +8 -0
  13. package/dist/commerce/client-runtime.js +7 -0
  14. package/dist/commerce/config.d.ts +7 -0
  15. package/dist/commerce/config.js +164 -0
  16. package/dist/commerce/envar-runtime.d.ts +46 -0
  17. package/dist/commerce/envar-runtime.js +256 -0
  18. package/dist/commerce/envar.d.ts +126 -0
  19. package/dist/commerce/envar.js +647 -0
  20. package/dist/commerce/http.d.ts +2 -0
  21. package/dist/commerce/http.js +34 -0
  22. package/dist/commerce/index.d.ts +4 -0
  23. package/dist/commerce/index.js +4 -0
  24. package/dist/commerce/management.d.ts +14 -0
  25. package/dist/commerce/management.js +138 -0
  26. package/dist/commerce/mpp-client.d.ts +59 -0
  27. package/dist/commerce/mpp-client.js +24 -0
  28. package/dist/commerce/mpp-runtime.d.ts +3 -0
  29. package/dist/commerce/mpp-runtime.js +3 -0
  30. package/dist/commerce/mpp.d.ts +53 -0
  31. package/dist/commerce/mpp.js +163 -0
  32. package/dist/commerce/peer-proxy.d.ts +44 -0
  33. package/dist/commerce/peer-proxy.js +667 -0
  34. package/dist/commerce/pricing.d.ts +12 -0
  35. package/dist/commerce/pricing.js +60 -0
  36. package/dist/commerce/runtime.d.ts +8 -0
  37. package/dist/commerce/runtime.js +8 -0
  38. package/dist/commerce/server.d.ts +58 -0
  39. package/dist/commerce/server.js +485 -0
  40. package/dist/commerce/store.d.ts +86 -0
  41. package/dist/commerce/store.js +466 -0
  42. package/dist/commerce/stripe-provider.d.ts +9 -0
  43. package/dist/commerce/stripe-provider.js +97 -0
  44. package/dist/commerce/types.d.ts +113 -0
  45. package/dist/commerce/types.js +8 -0
  46. package/dist/commerce/upstream.d.ts +6 -0
  47. package/dist/commerce/upstream.js +103 -0
  48. package/dist/commerce/vault.d.ts +8 -0
  49. package/dist/commerce/vault.js +55 -0
  50. package/dist/commerce/x402.d.ts +37 -0
  51. package/dist/commerce/x402.js +174 -0
  52. package/examples/buyer-policy.json +18 -0
  53. package/examples/envar-integration-policy.json +18 -0
  54. package/examples/seller.json +52 -0
  55. package/package.json +75 -10
  56. package/schemas/buyer-policy.schema.json +148 -0
  57. package/schemas/envarpay.config.schema.json +376 -0
  58. package/schemas/quote.schema.json +91 -0
@@ -0,0 +1,667 @@
1
+ /** Private, locally bound A2A peer access for native agents that lack a payment transport hook. */
2
+ import { randomUUID } from "node:crypto";
3
+ import { chmodSync, existsSync, lstatSync, mkdirSync } from "node:fs";
4
+ import { join, resolve } from "node:path";
5
+ import { DatabaseSync } from "node:sqlite";
6
+ import { createServer } from "node:http";
7
+ import { Readable } from "node:stream";
8
+ import { setTimeout as sleep } from "node:timers/promises";
9
+ import { AgentCard, Task, Role, } from "@a2a-js/sdk";
10
+ import { DefaultRequestHandler, JsonRpcTransportHandler, ServerCallContext, } from "@a2a-js/sdk/server";
11
+ import { RequestMalformedError, TaskNotFoundError, UnsupportedOperationError, } from "@a2a-js/sdk/errors";
12
+ import { CommerceError } from "./types.js";
13
+ import { digest, validateUrl } from "./config.js";
14
+ import { bearerAuthenticator } from "./server.js";
15
+ const loopback = (name) => ["localhost", "127.0.0.1", "[::1]", "::1"].includes(name);
16
+ const ref = /^[a-zA-Z0-9][a-zA-Z0-9._:-]{0,127}$/;
17
+ const safeHeaders = {
18
+ "Cache-Control": "no-store",
19
+ "X-Content-Type-Options": "nosniff",
20
+ "A2A-Version": "1.0",
21
+ };
22
+ function fail(code, message) {
23
+ throw new CommerceError(code, message);
24
+ }
25
+ function object(value) {
26
+ return !!value && typeof value === "object" && !Array.isArray(value);
27
+ }
28
+ function envelope(params) {
29
+ const message = params.message;
30
+ if (!message ||
31
+ message.role !== Role.ROLE_USER ||
32
+ !message.messageId ||
33
+ message.messageId.length > 128 ||
34
+ message.parts.length !== 1 ||
35
+ message.referenceTaskIds.length)
36
+ throw new RequestMalformedError("Send one user Part with a stable requestId and input object");
37
+ const part = message.parts[0].content;
38
+ let data;
39
+ if (part?.$case === "data")
40
+ data = part.value;
41
+ else if (part?.$case === "text") {
42
+ try {
43
+ data = JSON.parse(part.value);
44
+ }
45
+ catch {
46
+ throw new RequestMalformedError("Native text must contain JSON {requestId,input}");
47
+ }
48
+ }
49
+ else
50
+ throw new RequestMalformedError("Use one JSON data or text Part");
51
+ if (!object(data) ||
52
+ Object.keys(data).some((key) => !["requestId", "input"].includes(key)) ||
53
+ typeof data.requestId !== "string" ||
54
+ !ref.test(data.requestId) ||
55
+ !object(data.input))
56
+ throw new RequestMalformedError("Use exactly {requestId,input}; target and approval are local policy");
57
+ return { requestId: data.requestId, input: data.input };
58
+ }
59
+ class PeerHandler extends DefaultRequestHandler {
60
+ host;
61
+ constructor(host, card, store) {
62
+ const executor = {
63
+ execute: async () => {
64
+ throw new UnsupportedOperationError("Use the bound peer request");
65
+ },
66
+ cancelTask: async () => {
67
+ throw new UnsupportedOperationError("Cancellation is not available");
68
+ },
69
+ };
70
+ super(card, store, executor);
71
+ this.host = host;
72
+ }
73
+ async sendMessage(params, context) {
74
+ return this.host.send(params, context.user.userName);
75
+ }
76
+ async getTask(params, context) {
77
+ return this.host.read(params.id, context.user.userName);
78
+ }
79
+ }
80
+ /** The proxy token is never a wallet-management token. One proxy is bound to one approved peer/offer. */
81
+ export class A2APeerProxy {
82
+ options;
83
+ origin;
84
+ card;
85
+ db;
86
+ lock;
87
+ rpc;
88
+ authenticate;
89
+ cardUrl;
90
+ waitMs;
91
+ active = new Map();
92
+ stopped = false;
93
+ closed = false;
94
+ constructor(options) {
95
+ this.options = options;
96
+ const url = validateUrl(options.origin, true);
97
+ if (url.pathname !== "/" ||
98
+ url.search ||
99
+ (!loopback(url.hostname) &&
100
+ url.protocol !== "https:" &&
101
+ options.allowPrivateHttp !== true))
102
+ fail("proxy_origin", "Use loopback/HTTPS or explicitly isolated private HTTP");
103
+ this.origin = url.origin;
104
+ this.waitMs = options.waitMilliseconds ?? 20000;
105
+ if (!Number.isInteger(this.waitMs) ||
106
+ this.waitMs < 0 ||
107
+ this.waitMs > 25000)
108
+ fail("proxy_wait", "Proxy wait must be between zero and 25000 milliseconds");
109
+ if (!ref.test(options.buyerCaller) || !ref.test(options.offerId))
110
+ fail("proxy_binding", "Configure a fixed buyer identity and offer");
111
+ const peers = options.buyer.policy.peers.filter((peer) => peer.id === options.peerId);
112
+ if (peers.length !== 1)
113
+ fail("proxy_peer", "Bind exactly one locally approved peer");
114
+ this.cardUrl = peers[0].cardUrl;
115
+ this.authenticate = bearerAuthenticator(options.tokens);
116
+ const directory = resolve(options.stateDirectory);
117
+ mkdirSync(directory, { recursive: true, mode: 0o700 });
118
+ const stat = lstatSync(directory);
119
+ if (!stat.isDirectory() ||
120
+ stat.isSymbolicLink() ||
121
+ (stat.mode & 0o077) !== 0)
122
+ fail("proxy_state", "Use an owner-only proxy state directory");
123
+ for (const name of ["owner.sqlite", "proxy.sqlite"]) {
124
+ const path = join(directory, name);
125
+ if (existsSync(path)) {
126
+ const file = lstatSync(path);
127
+ if (!file.isFile() ||
128
+ file.isSymbolicLink() ||
129
+ (file.mode & 0o077) !== 0)
130
+ fail("proxy_state", "Proxy state must be private regular files");
131
+ }
132
+ }
133
+ this.lock = new DatabaseSync(join(directory, "owner.sqlite"));
134
+ chmodSync(join(directory, "owner.sqlite"), 0o600);
135
+ try {
136
+ this.lock.exec("PRAGMA busy_timeout=0;BEGIN EXCLUSIVE;CREATE TABLE IF NOT EXISTS owner(id INTEGER PRIMARY KEY)");
137
+ }
138
+ catch {
139
+ this.lock.close();
140
+ return fail("proxy_owned", "Another process owns this proxy state");
141
+ }
142
+ let database;
143
+ try {
144
+ database = new DatabaseSync(join(directory, "proxy.sqlite"));
145
+ chmodSync(join(directory, "proxy.sqlite"), 0o600);
146
+ database.exec(`PRAGMA journal_mode=DELETE;PRAGMA synchronous=FULL;
147
+ CREATE TABLE IF NOT EXISTS binding(id INTEGER PRIMARY KEY,value TEXT NOT NULL);
148
+ CREATE TABLE IF NOT EXISTS tasks(id TEXT PRIMARY KEY,caller TEXT NOT NULL,request_ref TEXT NOT NULL,input_digest TEXT NOT NULL,input_json TEXT NOT NULL,context_id TEXT NOT NULL,buyer_id TEXT NOT NULL DEFAULT '',phase TEXT NOT NULL,task_json TEXT NOT NULL DEFAULT '{}',UNIQUE(caller,request_ref));
149
+ CREATE TABLE IF NOT EXISTS continuations(task_id TEXT NOT NULL,request_ref TEXT NOT NULL,message_id TEXT NOT NULL,input_digest TEXT NOT NULL,phase TEXT NOT NULL,PRIMARY KEY(task_id,request_ref));`);
150
+ const binding = digest({
151
+ origin: this.origin,
152
+ buyerCaller: options.buyerCaller,
153
+ peerId: options.peerId,
154
+ cardUrl: this.cardUrl,
155
+ offerId: options.offerId,
156
+ }), saved = database
157
+ .prepare("SELECT value FROM binding WHERE id=1")
158
+ .get();
159
+ if (saved && saved.value !== binding)
160
+ fail("proxy_binding", "Original proxy state belongs to a different target or wallet identity");
161
+ database
162
+ .prepare("INSERT OR IGNORE INTO binding VALUES (1,?)")
163
+ .run(binding);
164
+ this.db = database;
165
+ }
166
+ catch (error) {
167
+ database?.close();
168
+ this.lock.exec("ROLLBACK");
169
+ this.lock.close();
170
+ throw error;
171
+ }
172
+ this.card = AgentCard.fromJSON({
173
+ name: options.label ?? "Approved peer service",
174
+ description: "Private fixed-peer access. Send JSON {requestId,input}; reuse requestId for the same intended task. Local wallet policy controls payment approval.",
175
+ version: "1",
176
+ supportedInterfaces: [
177
+ {
178
+ url: this.origin + "/a2a",
179
+ protocolBinding: "JSONRPC",
180
+ protocolVersion: "1.0",
181
+ },
182
+ ],
183
+ capabilities: { streaming: false, pushNotifications: false },
184
+ defaultInputModes: ["application/json", "text/plain"],
185
+ defaultOutputModes: ["text/plain", "application/json"],
186
+ skills: [
187
+ {
188
+ id: options.peerId,
189
+ name: options.label ?? options.peerId,
190
+ description: "Use a stable requestId for retry safety and the declared input of the approved service. GetTask retrieves the original result without approving payment.",
191
+ tags: ["a2a", "private-peer"],
192
+ examples: [
193
+ '{"requestId":"research-job-001","input":{"request":"Summarize the sources"}}',
194
+ ],
195
+ },
196
+ ],
197
+ securitySchemes: {
198
+ bearer: { httpAuthSecurityScheme: { scheme: "bearer" } },
199
+ },
200
+ securityRequirements: [{ schemes: { bearer: { list: [] } } }],
201
+ });
202
+ const store = {
203
+ load: (id, ctx) => this.read(id, ctx.user.userName),
204
+ save: async () => {
205
+ throw new UnsupportedOperationError("Tasks are saved with the original buyer purchase");
206
+ },
207
+ list: async () => {
208
+ throw new UnsupportedOperationError("Task listing is not exposed");
209
+ },
210
+ };
211
+ this.rpc = new JsonRpcTransportHandler(new PeerHandler(this, this.card, store));
212
+ }
213
+ row(id, caller) {
214
+ const row = this.db
215
+ .prepare("SELECT * FROM tasks WHERE id=? AND caller=?")
216
+ .get(id, caller);
217
+ if (!row)
218
+ throw new TaskNotFoundError("Original peer Task not found");
219
+ return row;
220
+ }
221
+ phase(id, phase, buyerId) {
222
+ if (buyerId === undefined)
223
+ this.db.prepare("UPDATE tasks SET phase=? WHERE id=?").run(phase, id);
224
+ else
225
+ this.db
226
+ .prepare("UPDATE tasks SET phase=?,buyer_id=? WHERE id=?")
227
+ .run(phase, buyerId, id);
228
+ }
229
+ projection(row, snapshot, code) {
230
+ let raw = snapshot?.task
231
+ ? structuredClone(snapshot.task)
232
+ : {};
233
+ if (!code && ["failed", "unknown"].includes(String(row.phase))) {
234
+ try {
235
+ const previous = JSON.parse(String(row.task_json));
236
+ if (typeof previous.metadata?.errorCode === "string")
237
+ code = previous.metadata.errorCode;
238
+ }
239
+ catch { }
240
+ }
241
+ if (code && !/^[a-z][a-z0-9_]{0,63}$/.test(code))
242
+ code = "peer_request_failed";
243
+ const quote = snapshot?.quote, phase = String(row.phase), needsApproval = phase === "waiting_owner" && snapshot?.state === "previewed";
244
+ const unknown = ((phase === "unknown" ||
245
+ (phase === "confirming" && !this.active.has(String(row.id)))) &&
246
+ !["completed", "failed", "canceled", "rejected"].includes(snapshot?.executionState ?? "")) ||
247
+ snapshot?.state === "unknown";
248
+ if (!snapshot?.task) {
249
+ const failed = phase === "failed" || snapshot?.state === "rejected";
250
+ raw = {
251
+ status: {
252
+ state: needsApproval
253
+ ? "TASK_STATE_AUTH_REQUIRED"
254
+ : failed
255
+ ? "TASK_STATE_FAILED"
256
+ : "TASK_STATE_WORKING",
257
+ },
258
+ };
259
+ }
260
+ const status = (raw.status ?? {});
261
+ let message = "";
262
+ if (needsApproval)
263
+ message = `Owner approval required. Review original buyer purchase ${row.buyer_id} with the separate private wallet console. No payment was approved by this agent. Proxy Task: ${row.id}.`;
264
+ else if (unknown)
265
+ message = `Original outcome is uncertain. Owner should inspect/recover buyer purchase ${row.buyer_id || "with original message " + row.id}. Do not create a replacement requestId. Proxy Task: ${row.id}.`;
266
+ else if (code)
267
+ message = `Peer operation needs attention (${code}). Proxy Task: ${row.id}.`;
268
+ else if (!snapshot?.task)
269
+ message = `Original peer purchase is ${snapshot?.executionState ?? phase}. Read GetTask ${row.id} for progress.`;
270
+ if (message)
271
+ status.message = {
272
+ messageId: `status-${row.id}`,
273
+ role: "ROLE_AGENT",
274
+ taskId: row.id,
275
+ contextId: row.context_id,
276
+ parts: [{ text: message }],
277
+ };
278
+ const task = Task.fromJSON({
279
+ ...raw,
280
+ id: row.id,
281
+ contextId: row.context_id,
282
+ status,
283
+ metadata: {
284
+ proxyRequestId: row.request_ref,
285
+ buyerPurchaseId: row.buyer_id,
286
+ ...(code ? { errorCode: code } : {}),
287
+ ...(quote
288
+ ? {
289
+ payment: {
290
+ state: snapshot.paymentState,
291
+ amount: quote.amount,
292
+ currency: quote.currency,
293
+ recipient: quote.recipient,
294
+ },
295
+ }
296
+ : {}),
297
+ ...(needsApproval ? { approvalRequired: true } : {}),
298
+ ...(unknown ? { recoveryRequired: true } : {}),
299
+ },
300
+ });
301
+ for (const entry of [
302
+ ...task.history,
303
+ ...(task.status?.message ? [task.status.message] : []),
304
+ ]) {
305
+ entry.taskId = task.id;
306
+ entry.contextId = task.contextId;
307
+ }
308
+ this.db
309
+ .prepare("UPDATE tasks SET task_json=? WHERE id=?")
310
+ .run(JSON.stringify(Task.toJSON(task)), row.id);
311
+ return task;
312
+ }
313
+ async exclusive(id, action) {
314
+ const existing = this.active.get(id);
315
+ if (existing)
316
+ return existing;
317
+ const running = action();
318
+ this.active.set(id, running);
319
+ try {
320
+ return await running;
321
+ }
322
+ finally {
323
+ this.active.delete(id);
324
+ }
325
+ }
326
+ snapshot(row) {
327
+ return row.buyer_id
328
+ ? this.options.buyer.get(this.options.buyerCaller, String(row.buyer_id))
329
+ : undefined;
330
+ }
331
+ async wait(row, snapshot, immediate) {
332
+ if (immediate)
333
+ return this.projection(this.row(String(row.id), String(row.caller)), snapshot);
334
+ const deadline = Date.now() + this.waitMs;
335
+ let value = snapshot;
336
+ while (!this.stopped &&
337
+ Date.now() < deadline &&
338
+ ["working", "queued", "dispatching", "not_started"].includes(value.executionState) &&
339
+ !["previewed", "unknown", "rejected"].includes(value.state)) {
340
+ await sleep(250);
341
+ value = this.options.buyer.get(this.options.buyerCaller, String(row.buyer_id));
342
+ }
343
+ return this.projection(this.row(String(row.id), String(row.caller)), value);
344
+ }
345
+ async send(params, caller) {
346
+ if (this.stopped)
347
+ fail("proxy_stopping", "Peer proxy is stopping");
348
+ const body = envelope(params), message = params.message;
349
+ if (message.taskId) {
350
+ const row = this.row(message.taskId, caller);
351
+ if (!row.buyer_id)
352
+ throw new UnsupportedOperationError("Original purchase is not ready for clarification");
353
+ return this.exclusive(String(row.id), async () => {
354
+ const prior = this.db
355
+ .prepare("SELECT * FROM continuations WHERE task_id=? AND request_ref=?")
356
+ .get(row.id, body.requestId);
357
+ if (prior) {
358
+ if (prior.input_digest !== digest(body.input))
359
+ fail("proxy_request_conflict", "Clarification reference already has different input");
360
+ return this.read(String(row.id), caller);
361
+ }
362
+ const messageId = randomUUID();
363
+ this.db
364
+ .prepare("INSERT INTO continuations VALUES (?,?,?,?,?)")
365
+ .run(row.id, body.requestId, messageId, digest(body.input), "sending");
366
+ try {
367
+ const snapshot = await this.options.buyer.continue(this.options.buyerCaller, String(row.buyer_id), { messageId, input: body.input });
368
+ this.db
369
+ .prepare("UPDATE continuations SET phase='observed' WHERE task_id=? AND request_ref=?")
370
+ .run(row.id, body.requestId);
371
+ return this.projection(row, snapshot);
372
+ }
373
+ catch (error) {
374
+ this.db
375
+ .prepare("UPDATE continuations SET phase='unknown' WHERE task_id=? AND request_ref=?")
376
+ .run(row.id, body.requestId);
377
+ this.phase(String(row.id), "unknown");
378
+ return this.projection(this.row(String(row.id), caller), this.snapshot(row), error instanceof CommerceError
379
+ ? error.code
380
+ : "continuation_unknown");
381
+ }
382
+ });
383
+ }
384
+ let row = this.db
385
+ .prepare("SELECT * FROM tasks WHERE caller=? AND request_ref=?")
386
+ .get(caller, body.requestId);
387
+ if (row && row.input_digest !== digest(body.input))
388
+ fail("proxy_request_conflict", "Original requestId already identifies different input");
389
+ if (!row) {
390
+ const id = randomUUID(), contextId = message.contextId && ref.test(message.contextId)
391
+ ? message.contextId
392
+ : randomUUID();
393
+ this.db
394
+ .prepare("INSERT INTO tasks(id,caller,request_ref,input_digest,input_json,context_id,phase) VALUES (?,?,?,?,?,?,?)")
395
+ .run(id, caller, body.requestId, digest(body.input), JSON.stringify(body.input), contextId, "created");
396
+ row = this.row(id, caller);
397
+ }
398
+ const id = String(row.id);
399
+ const running = this.exclusive(id, async () => {
400
+ let current = this.row(id, caller), snapshot = this.snapshot(current);
401
+ if (current.phase === "confirming") {
402
+ this.phase(id, "unknown");
403
+ current = this.row(id, caller);
404
+ }
405
+ if (current.phase === "waiting_owner" ||
406
+ current.phase === "tracking" ||
407
+ current.phase === "unknown" ||
408
+ current.phase === "failed")
409
+ return this.projection(current, snapshot);
410
+ try {
411
+ const allowed = this.options.buyer
412
+ .policySnapshot()
413
+ .peers.find((peer) => peer.id === this.options.peerId && peer.cardUrl === this.cardUrl);
414
+ if (!allowed)
415
+ fail("proxy_peer_changed", "The original fixed peer is no longer approved locally");
416
+ if (!snapshot) {
417
+ this.phase(id, "previewing");
418
+ snapshot = await this.options.buyer.preview(this.options.buyerCaller, {
419
+ cardUrl: this.cardUrl,
420
+ messageId: id,
421
+ offerId: this.options.offerId,
422
+ input: body.input,
423
+ });
424
+ this.phase(id, "previewed", snapshot.id);
425
+ current = this.row(id, caller);
426
+ }
427
+ // Direct local Buyer reference makes the policy check and confirm invocation one process boundary.
428
+ // The caller cannot supply a target, quoteToken, approval flag or policy override.
429
+ const policy = this.options.buyer.policySnapshot();
430
+ if (policy.approval !== "within_preapproved_limits") {
431
+ this.phase(id, "waiting_owner");
432
+ return this.projection(this.row(id, caller), snapshot);
433
+ }
434
+ const approvedPeer = policy.peers.find((peer) => peer.id === this.options.peerId && peer.cardUrl === this.cardUrl);
435
+ if (!approvedPeer)
436
+ fail("proxy_peer_changed", "The original fixed peer is no longer approved locally");
437
+ if (approvedPeer.protocol !== "free" && !policy.paymentsEnabled)
438
+ fail("payments_disabled", "Payments are disabled in local policy");
439
+ this.phase(id, "confirming");
440
+ snapshot = await this.options.buyer.confirm(this.options.buyerCaller, {
441
+ previewId: snapshot.id,
442
+ quoteToken: snapshot.quoteToken,
443
+ messageId: id,
444
+ });
445
+ this.phase(id, snapshot.state === "unknown" ? "unknown" : "tracking");
446
+ return this.wait(this.row(id, caller), snapshot, params.configuration?.returnImmediately === true);
447
+ }
448
+ catch (error) {
449
+ const state = this.row(id, caller).phase;
450
+ const definitelyBlocked = error instanceof CommerceError &&
451
+ [
452
+ "payments_disabled",
453
+ "budget_exceeded",
454
+ "approval_mismatch",
455
+ "quote_expired",
456
+ "policy_changed",
457
+ "proxy_peer_changed",
458
+ "peer_not_allowed",
459
+ "invalid_input",
460
+ "purchase_limit",
461
+ ].includes(error.code);
462
+ this.phase(id, definitelyBlocked
463
+ ? "failed"
464
+ : state === "confirming"
465
+ ? "unknown"
466
+ : state === "previewing"
467
+ ? "created"
468
+ : "failed");
469
+ return this.projection(this.row(id, caller), snapshot, error instanceof CommerceError ? error.code : "peer_request_failed");
470
+ }
471
+ });
472
+ if (params.configuration?.returnImmediately === true) {
473
+ void running.catch(() => undefined);
474
+ return this.projection(this.row(id, caller), this.snapshot(this.row(id, caller)));
475
+ }
476
+ let timer;
477
+ try {
478
+ return await Promise.race([
479
+ running,
480
+ new Promise((resolve) => {
481
+ timer = setTimeout(() => {
482
+ const current = this.row(id, caller);
483
+ resolve(this.projection(current, this.snapshot(current)));
484
+ }, this.waitMs);
485
+ }),
486
+ ]);
487
+ }
488
+ finally {
489
+ if (timer)
490
+ clearTimeout(timer);
491
+ }
492
+ }
493
+ async read(id, caller) {
494
+ const row = this.row(id, caller);
495
+ const snapshot = this.snapshot(row);
496
+ if (snapshot &&
497
+ snapshot.state !== "previewed" &&
498
+ snapshot.state !== "unknown" &&
499
+ ["waiting_owner", "unknown", "confirming"].includes(String(row.phase)))
500
+ this.phase(id, "tracking");
501
+ return this.projection(this.row(id, caller), snapshot);
502
+ }
503
+ async handle(request) {
504
+ const url = new URL(request.url);
505
+ if (url.origin !== this.origin ||
506
+ (request.headers.get("Host") && request.headers.get("Host") !== url.host))
507
+ return Response.json({ error: "invalid_host" }, { status: 421, headers: safeHeaders });
508
+ if (request.headers.get("Origin") &&
509
+ request.headers.get("Origin") !== this.origin)
510
+ return Response.json({ error: "origin_not_allowed" }, { status: 403, headers: safeHeaders });
511
+ if (request.method === "GET" &&
512
+ url.pathname === "/.well-known/agent-card.json" &&
513
+ !url.search)
514
+ return Response.json(AgentCard.toJSON(this.card), {
515
+ headers: safeHeaders,
516
+ });
517
+ if (request.method !== "POST" || url.pathname !== "/a2a" || url.search)
518
+ return Response.json({ error: "not_found" }, { status: 404, headers: safeHeaders });
519
+ const caller = await this.authenticate(request);
520
+ if (!caller)
521
+ return Response.json({ error: "authentication_required" }, {
522
+ status: 401,
523
+ headers: { ...safeHeaders, "WWW-Authenticate": "Bearer" },
524
+ });
525
+ if (this.stopped)
526
+ return Response.json({ error: "proxy_stopping" }, { status: 503, headers: safeHeaders });
527
+ if ([
528
+ "PAYMENT-SIGNATURE",
529
+ "Payment-Authorization",
530
+ "Payment-Receipt",
531
+ "PAYMENT-RESPONSE",
532
+ ].some((name) => request.headers.has(name)))
533
+ return Response.json({ error: "proxy_payment_credentials_forbidden" }, { status: 400, headers: safeHeaders });
534
+ let id = null;
535
+ try {
536
+ const version = request.headers.get("A2A-Version");
537
+ if (version && version !== "1.0")
538
+ throw new RequestMalformedError("This endpoint supports A2A 1.0 only");
539
+ if (!request.headers
540
+ .get("Content-Type")
541
+ ?.toLowerCase()
542
+ .startsWith("application/json"))
543
+ throw new RequestMalformedError("Send application/json");
544
+ const chunks = [];
545
+ let length = 0;
546
+ if (request.body) {
547
+ const reader = request.body.getReader();
548
+ while (true) {
549
+ const part = await reader.read();
550
+ if (part.done)
551
+ break;
552
+ length += part.value.byteLength;
553
+ if (length > 1048576) {
554
+ await reader.cancel();
555
+ return Response.json({ error: "request_too_large" }, { status: 413, headers: safeHeaders });
556
+ }
557
+ chunks.push(part.value);
558
+ }
559
+ }
560
+ const body = Buffer.concat(chunks).toString("utf8"), raw = JSON.parse(body);
561
+ id = raw?.id ?? null;
562
+ const validId = (typeof id === "string" && id.length > 0 && id.length <= 128) ||
563
+ (typeof id === "number" && Number.isSafeInteger(id));
564
+ if (!validId) {
565
+ id = null;
566
+ throw new RequestMalformedError("A bounded JSON-RPC request ID is required");
567
+ }
568
+ if (!object(raw) ||
569
+ raw.jsonrpc !== "2.0" ||
570
+ !((typeof id === "string" && id.length <= 128) ||
571
+ (typeof id === "number" && Number.isSafeInteger(id))) ||
572
+ !["SendMessage", "GetTask"].includes(String(raw.method)) ||
573
+ !object(raw.params))
574
+ throw new RequestMalformedError("Use one canonical SendMessage or GetTask request");
575
+ if (Object.keys(raw.params).some((key) => !["message", "configuration", "id", "historyLength"].includes(key)))
576
+ throw new RequestMalformedError("Unsupported routing metadata");
577
+ if (raw.method === "SendMessage") {
578
+ const m = raw.params.message, c = raw.params.configuration;
579
+ if (!object(m) ||
580
+ Object.keys(m).some((key) => !["messageId", "role", "parts", "contextId", "taskId"].includes(key)) ||
581
+ (c !== undefined &&
582
+ (!object(c) ||
583
+ Object.keys(c).some((key) => key !== "returnImmediately") ||
584
+ typeof c.returnImmediately !== "boolean")))
585
+ throw new RequestMalformedError("Unsupported message fields or configuration");
586
+ }
587
+ const result = await this.rpc.handle(body, new ServerCallContext({
588
+ user: { isAuthenticated: true, userName: caller },
589
+ requestedVersion: "1.0",
590
+ }));
591
+ if (Symbol.asyncIterator in result)
592
+ throw new UnsupportedOperationError("Streaming is not available");
593
+ return Response.json(result, { headers: safeHeaders });
594
+ }
595
+ catch (error) {
596
+ const code = error instanceof CommerceError ? error.code : "invalid_request";
597
+ return Response.json({
598
+ jsonrpc: "2.0",
599
+ id,
600
+ error: error instanceof CommerceError
601
+ ? { code: -32000, message: code }
602
+ : JsonRpcTransportHandler.mapToJSONRPCError(error),
603
+ }, { headers: safeHeaders });
604
+ }
605
+ }
606
+ assertListenHost(host) {
607
+ if (!loopback(host) &&
608
+ new URL(this.origin).protocol !== "https:" &&
609
+ this.options.allowPrivateHttp !== true)
610
+ fail("proxy_bind", "Non-loopback HTTP bind requires explicit private-network opt-in");
611
+ }
612
+ async stop() {
613
+ this.stopped = true;
614
+ await Promise.allSettled([...this.active.values()]);
615
+ }
616
+ close() {
617
+ if (this.closed)
618
+ return;
619
+ if (this.active.size)
620
+ fail("proxy_busy", "Stop and drain the proxy before closing");
621
+ this.db.close();
622
+ this.lock.exec("ROLLBACK");
623
+ this.lock.close();
624
+ this.closed = true;
625
+ }
626
+ }
627
+ export function listenPeerProxy(proxy, host = "127.0.0.1", port = 4030) {
628
+ proxy.assertListenHost(host);
629
+ const http = createServer(async (req, res) => {
630
+ try {
631
+ if (req.headers.host !== new URL(proxy.origin).host) {
632
+ res.writeHead(421, safeHeaders);
633
+ res.end('{"error":"invalid_host"}');
634
+ return;
635
+ }
636
+ const headers = new Headers();
637
+ for (const [key, value] of Object.entries(req.headers))
638
+ if (value !== undefined)
639
+ headers.set(key, Array.isArray(value) ? value.join(",") : value);
640
+ const request = new Request(new URL(req.url ?? "/", proxy.origin), {
641
+ method: req.method,
642
+ headers,
643
+ ...(req.method !== "GET" && req.method !== "HEAD"
644
+ ? {
645
+ body: Readable.toWeb(req),
646
+ duplex: "half",
647
+ }
648
+ : {}),
649
+ });
650
+ const response = await proxy.handle(request);
651
+ res.writeHead(response.status, Object.fromEntries(response.headers));
652
+ if (response.body)
653
+ Readable.fromWeb(response.body).pipe(res);
654
+ else
655
+ res.end();
656
+ }
657
+ catch {
658
+ if (!res.headersSent)
659
+ res.writeHead(500, safeHeaders);
660
+ res.end('{"error":"peer_request_failed"}');
661
+ }
662
+ });
663
+ http.requestTimeout = 30000;
664
+ http.headersTimeout = 15000;
665
+ http.listen(port, host);
666
+ return http;
667
+ }
@@ -0,0 +1,12 @@
1
+ import { type CommerceConfig, type Input, type PriceQuote } from './types.js';
2
+ export declare function buildQuote(config: CommerceConfig, args: {
3
+ serviceId: string;
4
+ offerId: string;
5
+ caller: string;
6
+ messageId: string;
7
+ input: Input;
8
+ now?: Date;
9
+ ttlSeconds?: number;
10
+ stripeRecipient?: string;
11
+ }): Readonly<PriceQuote>;
12
+ export declare function assertQuoteRequest(quote: Readonly<PriceQuote>, caller: string, messageId: string, input: Input, now?: Date): void;