@c4roa0/agentpay-user 0.0.0-stage → 0.3.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 (175) hide show
  1. package/AGENTS.md +137 -0
  2. package/CONTRIBUTING.md +16 -0
  3. package/LICENSE +373 -0
  4. package/NOTICE.md +20 -0
  5. package/README.md +85 -2
  6. package/SECURITY.md +16 -0
  7. package/doc/README.md +52 -0
  8. package/doc/TODO.md +107 -0
  9. package/doc/architecture.md +99 -0
  10. package/doc/design.md +60 -0
  11. package/doc/develope.md +68 -0
  12. package/doc/plans/user-wallet-distribution.md +91 -0
  13. package/doc/reference/harness-skill-runtime.md +120 -0
  14. package/doc/reference/local-cache-foundation.md +176 -0
  15. package/doc/reference/local-safe-owner-ui.md +25 -0
  16. package/doc/reference/local-update.md +18 -0
  17. package/doc/reference/multi-service-runtime.md +119 -0
  18. package/doc/reference/npm-integration-verification.md +103 -0
  19. package/doc/reference/onboarding-writing.md +48 -0
  20. package/doc/reference/original-order-recovery.md +97 -0
  21. package/doc/reference/private-anvil-onboarding-config.example.json +18 -0
  22. package/doc/reference/public-deployment-settings.md +56 -0
  23. package/doc/reference/random-skill-contract.md +18 -0
  24. package/doc/reference/result-file-cache.md +166 -0
  25. package/doc/reference/runtime-address-recovery.md +42 -0
  26. package/doc/reference/runtime-result-cache.md +157 -0
  27. package/doc/reference/runtime-startup-recovery.md +141 -0
  28. package/doc/reference/skill-input-output-cache.md +93 -0
  29. package/doc/reference/wallet-identity-policy-history.md +23 -0
  30. package/doc/runbooks/commands.md +103 -0
  31. package/doc/runbooks/deployment-authorization-update.md +42 -0
  32. package/doc/runbooks/emergency-payment-stop.md +80 -0
  33. package/doc/runbooks/local-autostart.md +212 -0
  34. package/doc/runbooks/local-cache.md +115 -0
  35. package/doc/runbooks/local-update.md +84 -0
  36. package/doc/runbooks/market-connection.md +73 -0
  37. package/doc/runbooks/npm-publication.md +56 -0
  38. package/doc/runbooks/official-test-package.md +62 -0
  39. package/doc/runbooks/order-cancellation.md +56 -0
  40. package/doc/runbooks/order-support.md +41 -0
  41. package/doc/runbooks/policy-renewal.md +80 -0
  42. package/doc/runbooks/private-anvil-terminal-setup.md +58 -0
  43. package/doc/runbooks/runtime-skill-client.md +155 -0
  44. package/doc/runbooks/safe-owner-web.md +104 -0
  45. package/doc/runbooks/safe-policy-migration.md +36 -0
  46. package/doc/runbooks/safe-policy-setup.md +52 -0
  47. package/doc/runbooks/terminal-authorization-preview.md +38 -0
  48. package/doc/runbooks/unlock-wallet.md +68 -0
  49. package/doc/runbooks/user-onboarding.md +87 -0
  50. package/doc/runbooks/wallet-identity.md +59 -0
  51. package/doc/security/public-source-review-2026-11-06.md +71 -0
  52. package/doc/usage.md +54 -0
  53. package/package.json +49 -4
  54. package/progress.md +210 -0
  55. package/run +29 -0
  56. package/scripts/agentpay-cli.mjs +11 -0
  57. package/scripts/authorize-existing-safe.mjs +211 -0
  58. package/scripts/autostart.mjs +186 -0
  59. package/scripts/cancel-order.mjs +76 -0
  60. package/scripts/check-installation.mjs +26 -0
  61. package/scripts/check-official-test-package.mjs +35 -0
  62. package/scripts/check-purchase-readiness.mjs +275 -0
  63. package/scripts/configure-runtime-client.mjs +16 -0
  64. package/scripts/guide-policy.mjs +64 -0
  65. package/scripts/initialize-caller-credential.mjs +11 -0
  66. package/scripts/install-skill-bundle.mjs +176 -0
  67. package/scripts/install-user.mjs +24 -0
  68. package/scripts/local-service.mjs +60 -0
  69. package/scripts/lock.mjs +11 -0
  70. package/scripts/login-notify.mjs +39 -0
  71. package/scripts/migrate-safe-policy.mjs +165 -0
  72. package/scripts/official-paid-test.mjs +9 -0
  73. package/scripts/poll-unconfirmed-notice.mjs +75 -0
  74. package/scripts/prepare-onboarding.mjs +33 -0
  75. package/scripts/preview-policy.mjs +65 -0
  76. package/scripts/random-model-ack-smoke.mjs +235 -0
  77. package/scripts/random-reject-smoke.mjs +147 -0
  78. package/scripts/renew.mjs +9 -0
  79. package/scripts/repair.mjs +10 -0
  80. package/scripts/revoke.mjs +9 -0
  81. package/scripts/select-skill-bundle.mjs +39 -0
  82. package/scripts/setup-local.mjs +199 -0
  83. package/scripts/status.mjs +13 -0
  84. package/scripts/test-package.mjs +35 -0
  85. package/scripts/unlock.mjs +13 -0
  86. package/scripts/update.mjs +7 -0
  87. package/scripts/user-setup.mjs +219 -0
  88. package/scripts/verify-npm-release.mjs +32 -0
  89. package/src/active-skill.mjs +160 -0
  90. package/src/anvil-onboarding-state.mjs +96 -0
  91. package/src/anvil-onboarding.mjs +1036 -0
  92. package/src/authorization-question-checkpoint.mjs +114 -0
  93. package/src/autostart-files.mjs +50 -0
  94. package/src/cache-management-cli.mjs +302 -0
  95. package/src/cache-management.mjs +47 -0
  96. package/src/caller-auth.mjs +58 -0
  97. package/src/client-request-lock.mjs +44 -0
  98. package/src/connect-market.mjs +408 -0
  99. package/src/demo-unlock-secret.mjs +64 -0
  100. package/src/deployment-status.mjs +198 -0
  101. package/src/file-cache-cli.mjs +83 -0
  102. package/src/file-cache-owner.mjs +74 -0
  103. package/src/file-cache-settings.mjs +50 -0
  104. package/src/file-cache.mjs +790 -0
  105. package/src/file-download.mjs +281 -0
  106. package/src/finalization-disclosure.mjs +16 -0
  107. package/src/guided-setup.mjs +194 -0
  108. package/src/index.mjs +1233 -0
  109. package/src/install-and-setup.mjs +38 -0
  110. package/src/install-preflight.mjs +201 -0
  111. package/src/install-user.mjs +279 -0
  112. package/src/installation-paths.mjs +31 -0
  113. package/src/invocation-result.mjs +148 -0
  114. package/src/invocation-store.mjs +385 -0
  115. package/src/local-data-directory.mjs +117 -0
  116. package/src/local-info.mjs +181 -0
  117. package/src/local-services.mjs +156 -0
  118. package/src/local-time.mjs +36 -0
  119. package/src/local-update.mjs +316 -0
  120. package/src/local-wallet-ipc.mjs +93 -0
  121. package/src/login-reminder.mjs +28 -0
  122. package/src/market-download-url.mjs +21 -0
  123. package/src/node-version.mjs +12 -0
  124. package/src/npm-update.mjs +126 -0
  125. package/src/official-paid-test.mjs +407 -0
  126. package/src/official-test-package.mjs +41 -0
  127. package/src/official-test-update.mjs +162 -0
  128. package/src/order-cancellation.mjs +241 -0
  129. package/src/order-closure.mjs +42 -0
  130. package/src/order-list-panel.mjs +110 -0
  131. package/src/order-review.mjs +424 -0
  132. package/src/order-status-panel.mjs +65 -0
  133. package/src/order-support.mjs +365 -0
  134. package/src/owner-approval-server.mjs +332 -0
  135. package/src/owner-signer.mjs +67 -0
  136. package/src/policy-info.mjs +38 -0
  137. package/src/policy-migration.mjs +84 -0
  138. package/src/policy-operations.mjs +67 -0
  139. package/src/policy-preview-server.mjs +236 -0
  140. package/src/policy-terminal-guide.mjs +377 -0
  141. package/src/prepare-local-services.mjs +143 -0
  142. package/src/prepare-onboarding.mjs +210 -0
  143. package/src/private-journal-files.mjs +29 -0
  144. package/src/protocol-package.mjs +25 -0
  145. package/src/readiness-profile.mjs +30 -0
  146. package/src/renew-policy.mjs +605 -0
  147. package/src/repair-market-wallet.mjs +137 -0
  148. package/src/restore-wallet.mjs +255 -0
  149. package/src/result-cache-view.mjs +276 -0
  150. package/src/runtime-client.mjs +1145 -0
  151. package/src/runtime-connection-settings.mjs +101 -0
  152. package/src/runtime-recovery.mjs +667 -0
  153. package/src/runtime-status.mjs +32 -0
  154. package/src/safe-owner-anvil.mjs +115 -0
  155. package/src/safe-policy-plan.mjs +195 -0
  156. package/src/service-reference.mjs +233 -0
  157. package/src/service-url.mjs +52 -0
  158. package/src/settlement-response.mjs +69 -0
  159. package/src/setup-autostart.mjs +197 -0
  160. package/src/setup-demo.mjs +152 -0
  161. package/src/skill-bundle.mjs +142 -0
  162. package/src/skill-client.mjs +198 -0
  163. package/src/skill-install-prompt.mjs +31 -0
  164. package/src/skill-package.mjs +50 -0
  165. package/src/skill-zip.mjs +220 -0
  166. package/src/software-versions.mjs +48 -0
  167. package/src/status-panel.mjs +603 -0
  168. package/src/transport.mjs +48 -0
  169. package/src/uninstall-local.mjs +299 -0
  170. package/src/user-cli.mjs +354 -0
  171. package/src/user-setup-guide.mjs +193 -0
  172. package/src/wallet-control.mjs +182 -0
  173. package/src/wallet-ipc-error.mjs +16 -0
  174. package/src/wallet-package.mjs +28 -0
  175. package/update-pair.json +6 -0
package/src/index.mjs ADDED
@@ -0,0 +1,1233 @@
1
+ import { runtimeSoftware } from "./software-versions.mjs";
2
+ // Authenticated local invocation engine. Preserve durable request/payment checkpoints before
3
+ // network calls; startup reconciliation observes history without creating replacement purchases.
4
+ // SPDX-License-Identifier: MPL-2.0
5
+ // This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
6
+ // If a copy was not distributed with this file, see https://mozilla.org/MPL/2.0/.
7
+ import { manageCache } from "./cache-management.mjs";
8
+ import { readCacheLimits } from "./file-cache-settings.mjs";
9
+ import { createFileCache } from "./file-cache.mjs";
10
+ import { assertNotArchived } from "./order-review.mjs";
11
+ import {
12
+ assertPreparedCommercialTerms,
13
+ normalizeAcceptedTerms,
14
+ } from "./service-reference.mjs";
15
+ import { serviceUrl } from "./service-url.mjs";
16
+ import { createRuntimeRecovery } from "./runtime-recovery.mjs";
17
+ import { constants } from "node:fs";
18
+ import { open } from "node:fs/promises";
19
+ import { assertOrderCanContinue } from "./order-closure.mjs";
20
+ import { invocationResults } from "./invocation-result.mjs";
21
+ import { normalizeRuntimeResult } from "./service-reference.mjs";
22
+ import {
23
+ parseServiceReference,
24
+ serviceContext,
25
+ assertPreparedService,
26
+ providerRequest,
27
+ providerOutput,
28
+ } from "./service-reference.mjs";
29
+ import { readSettlementResult as settlementResult } from "./settlement-response.mjs";
30
+ import { randomBytes } from "node:crypto";
31
+ import { createServer } from "node:http";
32
+ import { createConnection } from "node:net";
33
+ import { hashPrivateRequest } from "@c4roa0/agentpay-protocol";
34
+ import { createInvocationStore } from "./invocation-store.mjs";
35
+ import { authorizeCaller, readCallerToken } from "./caller-auth.mjs";
36
+ import { fetchJsonWithTimeout } from "./transport.mjs";
37
+ import { readFinalizationDisclosure } from "./finalization-disclosure.mjs";
38
+ import { walletIpcError } from "./wallet-ipc-error.mjs";
39
+
40
+ const host = String(process.env.AGENTPAY_USER_HOST || "127.0.0.1").trim();
41
+ if (!["127.0.0.1", "localhost", "::1"].includes(host))
42
+ throw Error("User Runtime must listen on loopback");
43
+ const originHost = host.includes(":") ? `[${host}]` : host;
44
+ const port = Number(process.env.AGENTPAY_USER_PORT || 6490);
45
+ const walletSocket = requiredEnv("AGENTPAY_WALLET_SOCKET");
46
+ const walletCapabilityFile = requiredEnv("AGENTPAY_WALLET_AGENT_TOKEN_FILE");
47
+ const callerCredentialFile = requiredEnv("AGENTPAY_USER_CALLER_TOKEN_FILE");
48
+ const settlementUrl = requiredEnv("AGENTPAY_SETTLEMENT_URL").replace(
49
+ /\/+$/,
50
+ "",
51
+ );
52
+ const operationTimeoutMs = Number(
53
+ process.env.AGENTPAY_USER_OPERATION_TIMEOUT_MS || 90_000,
54
+ );
55
+ if (!Number.isSafeInteger(operationTimeoutMs) || operationTimeoutMs < 1) {
56
+ throw new Error(
57
+ "AGENTPAY_USER_OPERATION_TIMEOUT_MS must be a positive number of milliseconds",
58
+ );
59
+ }
60
+ const includeProviderEvidence =
61
+ process.env.AGENTPAY_USER_INCLUDE_PROVIDER_EVIDENCE === "true";
62
+ const invocationStore = createInvocationStore(
63
+ requiredEnv("AGENTPAY_USER_STATE_FILE"),
64
+ );
65
+
66
+ let localFileCache;
67
+ async function getFileCache() {
68
+ if (localFileCache) return localFileCache;
69
+ const privateFileOrigins = JSON.parse(
70
+ process.env.AGENTPAY_USER_FILE_CACHE_PRIVATE_ORIGINS || "[]",
71
+ );
72
+ if (
73
+ !Array.isArray(privateFileOrigins) ||
74
+ privateFileOrigins.length > 16 ||
75
+ privateFileOrigins.some((origin) => {
76
+ try {
77
+ return (
78
+ typeof origin !== "string" ||
79
+ !["http:", "https:"].includes(new URL(origin).protocol) ||
80
+ new URL(origin).origin !== origin
81
+ );
82
+ } catch {
83
+ return true;
84
+ }
85
+ })
86
+ )
87
+ throw coded("FILE_SETTINGS_INVALID", "Invalid private file-cache origins.");
88
+ const path = requiredEnv("AGENTPAY_USER_STATE_FILE");
89
+ localFileCache = createFileCache(path, {
90
+ privateOrigins: privateFileOrigins,
91
+ limits: await readCacheLimits(path),
92
+ });
93
+ return localFileCache;
94
+ }
95
+
96
+ const resultStore = invocationResults(requiredEnv("AGENTPAY_USER_STATE_FILE"));
97
+ const invocationLocks = new Map();
98
+ let activeInvocations = 0;
99
+ const callerCredential = await readCallerToken(callerCredentialFile);
100
+ const recovery = createRuntimeRecovery({
101
+ store: invocationStore,
102
+ results: resultStore,
103
+ statePath: requiredEnv("AGENTPAY_USER_STATE_FILE"),
104
+ settlementUrl,
105
+ readWallet: () => callWallet("status", {}, 1500),
106
+ readOrder: (orderId, recoveryScope) =>
107
+ callWallet(
108
+ "readSettlementOrder",
109
+ { settlementUrl, orderId, ...(recoveryScope ? { recoveryScope } : {}) },
110
+ 35000,
111
+ ),
112
+ readRequest: (orderRequestId, recoveryScope) =>
113
+ callWallet(
114
+ "readSettlementOrder",
115
+ {
116
+ settlementUrl,
117
+ orderRequestId,
118
+ ...(recoveryScope ? { recoveryScope } : {}),
119
+ },
120
+ 35000,
121
+ ),
122
+ });
123
+
124
+ const server = createServer(async (req, res) => {
125
+ const url = new URL(req.url || "/", `http://${originHost}`);
126
+ if (req.method === "GET" && url.pathname === "/health") {
127
+ return json(res, 200, {
128
+ ok: true,
129
+ result: {
130
+ role: "user-runtime",
131
+ software: runtimeSoftware,
132
+ capabilities: ["agentpay.runtime-call.v1"],
133
+ walletTransport: "unix",
134
+ },
135
+ });
136
+ }
137
+ if (req.method === "GET" && url.pathname === "/status") {
138
+ if (!authorizeCaller(req.headers.authorization, callerCredential))
139
+ return json(res, 401, {
140
+ ok: false,
141
+ error: {
142
+ code: "UNAUTHORIZED",
143
+ message: "Valid caller credential required",
144
+ },
145
+ });
146
+ void recovery.refresh();
147
+ return json(res, 200, {
148
+ ok: true,
149
+ result: { role: "user-runtime", software: runtimeSoftware, ...recovery.status() },
150
+ });
151
+ }
152
+ if (
153
+ req.method !== "POST" ||
154
+ ![
155
+ "/services/invoke",
156
+ "/services/check",
157
+ "/services/offer",
158
+ "/files/cache",
159
+ "/files/list",
160
+ "/files/remove",
161
+ "/files/input",
162
+ "/files/input-release",
163
+ "/cache/manage",
164
+ "/services/history",
165
+ ].includes(url.pathname)
166
+ )
167
+ return json(res, 404, {
168
+ ok: false,
169
+ error: { code: "NOT_FOUND", message: "route not found" },
170
+ });
171
+ if (!authorizeCaller(req.headers.authorization, callerCredential))
172
+ return json(res, 401, {
173
+ ok: false,
174
+ error: {
175
+ code: "UNAUTHORIZED",
176
+ message: "valid caller credential is required",
177
+ },
178
+ });
179
+ let releaseInvocation = () => {};
180
+ try {
181
+ const body = await readJson(req);
182
+ if (url.pathname === "/cache/manage")
183
+ return json(res, 200, {
184
+ ok: true,
185
+ result: await manageCache(await getFileCache(), body),
186
+ });
187
+ if (url.pathname === "/files/input") {
188
+ if (
189
+ !body ||
190
+ Object.keys(body).sort().join(",") !== "files,requestId" ||
191
+ !/^[A-Za-z0-9._:-]{16,128}$/.test(body.requestId || "") ||
192
+ !Array.isArray(body.files) ||
193
+ body.files.length > 20
194
+ )
195
+ throw coded(
196
+ "FILE_INPUT_INVALID",
197
+ "Supply an original request ID and explicit input files.",
198
+ );
199
+ const keys = new Set();
200
+ for (const f of body.files) {
201
+ if (
202
+ !f ||
203
+ typeof f !== "object" ||
204
+ Array.isArray(f) ||
205
+ Object.keys(f).some(
206
+ (k) =>
207
+ !["fileKey", "sourcePath", "url", "filename", "sha256"].includes(
208
+ k,
209
+ ),
210
+ ) ||
211
+ typeof f.fileKey !== "string" ||
212
+ !/^[A-Za-z0-9._-]{1,80}$/.test(f.fileKey) ||
213
+ keys.has(f.fileKey) ||
214
+ (typeof f.sourcePath === "string") === (typeof f.url === "string")
215
+ )
216
+ throw coded(
217
+ "FILE_INPUT_INVALID",
218
+ "Declare distinct file keys and one source per input.",
219
+ );
220
+ keys.add(f.fileKey);
221
+ }
222
+ const cache = await getFileCache(),
223
+ rows = [];
224
+ for (const f of body.files)
225
+ rows.push(
226
+ await (f.sourcePath
227
+ ? cache.importInput({ ...f, requestId: body.requestId })
228
+ : cache.cache({ ...f, requestId: body.requestId })),
229
+ );
230
+ return json(res, 200, { ok: true, result: rows });
231
+ }
232
+ if (url.pathname === "/files/input-release") {
233
+ const context = serviceContext(
234
+ body,
235
+ process.env.AGENTPAY_USER_MAX_REQUEST_USAGE_UNITS || "1000000",
236
+ );
237
+ const requestHash = hashPrivateRequest(context.request),
238
+ requestId = req.headers["idempotency-key"];
239
+ const entry = await invocationStore.lookup({
240
+ invocationKey: requestId,
241
+ requestHash,
242
+ binding: context.binding,
243
+ routingVersion: "agentpay.runtime-call.v1",
244
+ });
245
+ const saved = await resultStore.read(entry);
246
+ if (!saved)
247
+ throw coded(
248
+ "FILE_RESULT_UNVERIFIED",
249
+ "Verified durable delivery is required before releasing input protection.",
250
+ );
251
+ normalizeRuntimeResult(saved, context, requestHash, entry.orderId);
252
+ return json(res, 200, {
253
+ ok: true,
254
+ result: await (await getFileCache()).releaseInputs(requestId),
255
+ });
256
+ }
257
+ if (url.pathname.startsWith("/files/")) {
258
+ const fileCache = await getFileCache();
259
+ const allowed =
260
+ url.pathname === "/files/cache"
261
+ ? ["orderId", "fileKey", "url", "filename", "sha256"]
262
+ : url.pathname === "/files/remove"
263
+ ? ["orderId", "fileKey", "confirmed"]
264
+ : ["orderId", "offset", "limit"];
265
+ if (
266
+ !body ||
267
+ typeof body !== "object" ||
268
+ Array.isArray(body) ||
269
+ Object.keys(body).some((k) => !allowed.includes(k)) ||
270
+ !/^0x[0-9a-f]{64}$/.test(body.orderId || "")
271
+ )
272
+ throw coded(
273
+ "FILE_INPUT_INVALID",
274
+ "Supply the original Order ID and file operation fields.",
275
+ );
276
+ if (url.pathname !== "/files/cache") {
277
+ const result =
278
+ url.pathname === "/files/remove"
279
+ ? await fileCache.remove(body)
280
+ : await fileCache.list(body.orderId, body);
281
+ return json(res, 200, { ok: true, result });
282
+ }
283
+ const entries = Object.values(await invocationStore.inventory()).filter(
284
+ (v) => v.orderId === body.orderId,
285
+ );
286
+ if (entries.length !== 1)
287
+ throw coded(
288
+ "FILE_ORDER_UNAVAILABLE",
289
+ "No unique local result is associated with this Order.",
290
+ );
291
+ const entry = entries[0],
292
+ saved = await resultStore.read(entry);
293
+ if (!saved || entry.routingVersion !== "agentpay.runtime-call.v1")
294
+ throw coded(
295
+ "FILE_RESULT_UNVERIFIED",
296
+ "A verified local result is required before caching files.",
297
+ );
298
+ const binding = JSON.parse(entry.serviceBinding),
299
+ reference = parseServiceReference(binding.reference);
300
+ normalizeRuntimeResult(
301
+ saved,
302
+ {
303
+ reference,
304
+ skillId: reference.skillId,
305
+ profileHash: reference.profileHash,
306
+ requestedMaxUsageUnits: binding.requestedMaxUsageUnits,
307
+ },
308
+ entry.requestHash,
309
+ entry.orderId,
310
+ );
311
+ const result = await fileCache.cache(body);
312
+ return json(res, 200, { ok: true, result });
313
+ }
314
+
315
+ if (url.pathname === "/services/offer") {
316
+ if (!body || Object.keys(body).join(",") !== "service")
317
+ throw coded("INVALID_INPUT", "Supply a service reference");
318
+ const reference = parseServiceReference(body.service),
319
+ url = serviceUrl(settlementUrl, "/agentpay/purchase-readiness");
320
+ url.searchParams.set("skillId", reference.skillId);
321
+ url.searchParams.set("profileHash", reference.profileHash);
322
+ const offer = await fetchJsonWithTimeout(url, {}, 5000);
323
+ if (!offer.response.ok || offer.result?.ok !== true)
324
+ throw coded("OFFER_UNAVAILABLE", "Current offer could not be read");
325
+ return json(res, 200, { ok: true, result: offer.result.result });
326
+ }
327
+ if (url.pathname === "/services/check") {
328
+ if (!body || Object.keys(body).join(",") !== "service")
329
+ throw coded(
330
+ "INVALID_INPUT",
331
+ "Supply the public service reference only",
332
+ );
333
+ const reference = parseServiceReference(body.service);
334
+ return json(res, 200, {
335
+ ok: true,
336
+ result: {
337
+ role: "user-runtime",
338
+ service: reference,
339
+ runtimeIdentityMatches: true,
340
+ purchaseReadinessChecked: false,
341
+ orderCreated: false,
342
+ capabilities: ["agentpay.original-request.v1"],
343
+ },
344
+ });
345
+ }
346
+ const context = serviceContext(
347
+ body,
348
+ process.env.AGENTPAY_USER_MAX_REQUEST_USAGE_UNITS || "1000000",
349
+ );
350
+ const {
351
+ skillId: callSkillId,
352
+ profileHash: callProfileHash,
353
+ requestedMaxUsageUnits: callUsage,
354
+ } = context;
355
+ const input = { request: context.request };
356
+ const requestHash = hashPrivateRequest(input.request);
357
+ if (url.pathname === "/services/history") {
358
+ const entry = await invocationStore.lookup({
359
+ invocationKey: req.headers["idempotency-key"],
360
+ requestHash,
361
+ binding: context.binding,
362
+ routingVersion: "agentpay.runtime-call.v1",
363
+ });
364
+ const saved = await resultStore.read(entry);
365
+ const delivery = saved
366
+ ? normalizeRuntimeResult(saved, context, requestHash, entry.orderId)
367
+ : null;
368
+ return json(res, 200, {
369
+ ok: true,
370
+ result: {
371
+ orderRequestId: entry.orderRequestId,
372
+ orderId: entry.orderId,
373
+ recoveryScope: entry.recoveryScope || null,
374
+ delivery,
375
+ },
376
+ });
377
+ }
378
+ const originalOrderRequestId =
379
+ req.headers["x-agentpay-original-request"] ?? null;
380
+ if (
381
+ originalOrderRequestId !== null &&
382
+ !/^0x[0-9a-f]{64}$/.test(originalOrderRequestId)
383
+ )
384
+ throw coded("INVALID_INPUT", "Invalid original request reference.");
385
+ releaseInvocation = await acquireInvocation(req.headers["idempotency-key"]);
386
+ await recovery.assertLocalHistory();
387
+ let invocation = await invocationStore.begin({
388
+ invocationKey: req.headers["idempotency-key"],
389
+ requestHash,
390
+ binding: context.binding,
391
+ routingVersion: "agentpay.runtime-call.v1",
392
+ originalOrderRequestId,
393
+ readRecoveryScope: async () =>
394
+ (await callWallet("status", {}, 1500)).recoveryScope,
395
+ });
396
+ {
397
+ const savedResult = await resultStore.read(invocation);
398
+ if (savedResult)
399
+ return json(res, 200, {
400
+ ok: true,
401
+ reusedResult: true,
402
+ result: normalizeRuntimeResult(
403
+ savedResult,
404
+ context,
405
+ requestHash,
406
+ invocation.orderId,
407
+ ),
408
+ });
409
+ }
410
+ await assertNotArchived(
411
+ requiredEnv("AGENTPAY_USER_STATE_FILE"),
412
+ invocation,
413
+ );
414
+ const pendingResult = await resultStore.readPending(invocation);
415
+ if (pendingResult) {
416
+ const delivered = providerOutput(
417
+ { result: pendingResult },
418
+ context,
419
+ input.request,
420
+ invocation.orderId,
421
+ requestHash,
422
+ );
423
+ const evidence = await waitForDeliveryEvidence(
424
+ invocation.orderId,
425
+ delivered.outputHash,
426
+ invocation.recoveryScope,
427
+ context,
428
+ requestHash,
429
+ );
430
+ if (String(evidence.usageUnits) !== String(delivered.usageUnits))
431
+ throw coded(
432
+ "DELIVERY_EVIDENCE_MISMATCH",
433
+ "Settlement usage differs from the saved result",
434
+ );
435
+ const result = normalizeRuntimeResult(
436
+ {
437
+ version: "agentpay.runtime-result.v1",
438
+ orderId: invocation.orderId,
439
+ requestHash,
440
+ service: { skillId: callSkillId, profileHash: callProfileHash },
441
+ output: delivered.output,
442
+ outputHash: delivered.outputHash,
443
+ usageUnits: delivered.usageUnits,
444
+ deliveryEvidenceVerified: true,
445
+ finalization: evidence.finalization,
446
+ deadlines: evidence.deadlines,
447
+ },
448
+ context,
449
+ requestHash,
450
+ invocation.orderId,
451
+ );
452
+ await resultStore.save(invocation, result);
453
+ return json(res, 200, { ok: true, reusedResult: true, result });
454
+ }
455
+
456
+ if (
457
+ invocation.quoteAcceptanceRejected &&
458
+ !invocation.orderId &&
459
+ !invocation.paymentPayload
460
+ ) {
461
+ invocation = await invocationStore.update(
462
+ invocation.invocationId,
463
+ requestHash,
464
+ { acceptedTerms: null, quoteAcceptanceRejected: false },
465
+ );
466
+ }
467
+ if (
468
+ context.acceptedTerms &&
469
+ invocation.acceptedTerms &&
470
+ JSON.stringify(context.acceptedTerms) !==
471
+ JSON.stringify(invocation.acceptedTerms)
472
+ )
473
+ throw coded(
474
+ "QUOTE_TERMS_CONFLICT",
475
+ "This request already accepted different terms; preserve its original identity.",
476
+ );
477
+ recovery.assertInvocation(invocation);
478
+ if (invocation.orderId) await assertOriginalOrderOpen(invocation.orderId);
479
+ if (!invocation.paymentPayload) {
480
+ if (
481
+ !invocation.orderId &&
482
+ !invocation.acceptedTerms &&
483
+ !invocation.quoteAcceptanceRequired
484
+ )
485
+ throw coded(
486
+ "RUNTIME_RECOVERY_REQUIRED",
487
+ "An older preparation may already exist. Preserve the original request for review; do not create a replacement purchase.",
488
+ );
489
+ if (!invocation.orderId && !invocation.acceptedTerms) {
490
+ const url = serviceUrl(settlementUrl, "/agentpay/purchase-readiness");
491
+ url.searchParams.set("skillId", callSkillId);
492
+ url.searchParams.set("profileHash", callProfileHash);
493
+ const response = await fetchJsonWithTimeout(
494
+ url.toString(),
495
+ {},
496
+ operationTimeoutMs,
497
+ );
498
+ const snapshot = response.result?.result || response.result;
499
+ if (
500
+ !response.response.ok ||
501
+ !snapshot?.commercialReady ||
502
+ !snapshot.quote
503
+ )
504
+ throw coded(
505
+ "PURCHASE_NOT_READY",
506
+ "Current service offer is unavailable",
507
+ );
508
+ const terms =
509
+ context.acceptedTerms ||
510
+ normalizeAcceptedTerms({
511
+ baseAmountAtomic: String(snapshot.quote.baseAmountAtomic),
512
+ unitPriceAtomic: String(snapshot.quote.unitPriceAtomic),
513
+ serviceWindowSeconds:
514
+ snapshot.orderDeadlinePolicy?.serviceWindowSeconds,
515
+ });
516
+ if (
517
+ !Number.isSafeInteger(terms.serviceWindowSeconds) ||
518
+ terms.serviceWindowSeconds < 1
519
+ )
520
+ throw coded(
521
+ "QUOTE_TERMS_INVALID",
522
+ "Current service duration is unavailable",
523
+ );
524
+ invocation = await invocationStore.update(
525
+ invocation.invocationId,
526
+ requestHash,
527
+ { acceptedTerms: terms },
528
+ );
529
+ }
530
+ if (!invocation.recoveryScope)
531
+ throw coded(
532
+ "RUNTIME_RECOVERY_REQUIRED",
533
+ "Original authorization was not saved. Preserve this request for review.",
534
+ );
535
+ const activeScope = (await callWallet("status", {}, 1500)).recoveryScope;
536
+ if (
537
+ !activeScope ||
538
+ ["network", "module", "safe", "policyId"].some(
539
+ (k) =>
540
+ String(activeScope[k]).toLowerCase() !==
541
+ String(invocation.recoveryScope[k]).toLowerCase(),
542
+ )
543
+ )
544
+ throw coded(
545
+ "RUNTIME_RECOVERY_REQUIRED",
546
+ "This request uses an earlier authorization. Keep its original identity.",
547
+ );
548
+ await assertNotArchived(
549
+ requiredEnv("AGENTPAY_USER_STATE_FILE"),
550
+ invocation,
551
+ );
552
+ let prepared;
553
+ try {
554
+ prepared = settlementResult(
555
+ await callWallet("prepareSettlementOrder", {
556
+ settlementUrl,
557
+ orderRequestId: invocation.orderRequestId,
558
+ expectedScope: invocation.recoveryScope,
559
+ skillId: callSkillId,
560
+ profileHash: callProfileHash,
561
+ requestHash,
562
+ requestedMaxUsageUnits: callUsage,
563
+ ...(invocation.acceptedTerms
564
+ ? { acceptedTerms: invocation.acceptedTerms }
565
+ : {}),
566
+ }),
567
+ );
568
+ } catch (error) {
569
+ if (
570
+ error.code === "QUOTE_TERMS_CHANGED" &&
571
+ !invocation.orderId &&
572
+ !invocation.paymentPayload
573
+ )
574
+ await invocationStore.update(invocation.invocationId, requestHash, {
575
+ quoteAcceptanceRejected: true,
576
+ });
577
+ throw error;
578
+ }
579
+ // Keep the Order identity even if preparation/polling is interrupted.
580
+ invocation = await invocationStore.update(
581
+ invocation.invocationId,
582
+ requestHash,
583
+ { orderId: prepared.order.orderId },
584
+ );
585
+ assertOrderCanContinue(prepared);
586
+ const payable = await waitForPaymentRequired(prepared);
587
+ assertPreparedService(payable, context, requestHash);
588
+ assertPreparedCommercialTerms(payable, invocation.acceptedTerms);
589
+ const requirements = payable.paymentRequired;
590
+ invocation = await invocationStore.update(
591
+ invocation.invocationId,
592
+ requestHash,
593
+ { orderId: requirements.extra.orderId },
594
+ );
595
+ let signed;
596
+ try {
597
+ signed = await signCommitment(
598
+ requirements,
599
+ payable,
600
+ invocation.commitmentNonce,
601
+ context,
602
+ );
603
+ } catch (error) {
604
+ if (error?.code !== "DUPLICATE_REQUEST") throw error;
605
+ invocation = await invocationStore.update(
606
+ invocation.invocationId,
607
+ requestHash,
608
+ { commitmentNonce: `0x${randomBytes(32).toString("hex")}` },
609
+ );
610
+ signed = await signCommitment(
611
+ requirements,
612
+ payable,
613
+ invocation.commitmentNonce,
614
+ context,
615
+ );
616
+ }
617
+ const validBefore = signed.validBefore;
618
+ const paymentPayload = {
619
+ x402Version: 2,
620
+ accepted: requirements,
621
+ payload: {
622
+ commitment: {
623
+ binding: "agentpay.safe-module.v1",
624
+ safe: requirements.extra.safe,
625
+ policyId: requirements.extra.policyId,
626
+ factory: requirements.extra.factory,
627
+ orderId: requirements.extra.orderId,
628
+ vault: requirements.payTo,
629
+ token: requirements.asset,
630
+ amount: requirements.amount,
631
+ termsHash: requirements.extra.termsHash,
632
+ scopeProofHash: requirements.extra.scopeProofHash,
633
+ commitmentNonce: invocation.commitmentNonce,
634
+ validBefore: String(validBefore),
635
+ },
636
+ signature: signed.signature,
637
+ },
638
+ };
639
+ invocation = await invocationStore.update(
640
+ invocation.invocationId,
641
+ requestHash,
642
+ { paymentPayload },
643
+ );
644
+ }
645
+ const orderId = invocation.orderId;
646
+ if (!invocation.paymentSubmitted) {
647
+ await assertOriginalOrderOpen(orderId);
648
+ await assertNotArchived(
649
+ requiredEnv("AGENTPAY_USER_STATE_FILE"),
650
+ invocation,
651
+ );
652
+ await submitPayment(
653
+ invocation.paymentPayload,
654
+ orderId,
655
+ requestHash,
656
+ input.request,
657
+ context,
658
+ );
659
+ invocation = await invocationStore.update(
660
+ invocation.invocationId,
661
+ requestHash,
662
+ { paymentSubmitted: true },
663
+ );
664
+ }
665
+ const readiness = await waitForServiceReady(orderId, requestHash);
666
+ if (
667
+ readiness?.skillId !== context.skillId ||
668
+ readiness?.profileHash !== context.profileHash ||
669
+ readiness?.orderId !== orderId ||
670
+ readiness?.requestHash !== requestHash ||
671
+ BigInt(readiness.maxUsageUnits) > BigInt(context.requestedMaxUsageUnits)
672
+ )
673
+ throw coded(
674
+ "SERVICE_AUTHORIZATION_MISMATCH",
675
+ "Service Authorization differs from the bound Order",
676
+ );
677
+ if (!readiness?.authorization)
678
+ throw coded(
679
+ "SERVICE_NOT_READY",
680
+ "Wallet did not return a Service Authorization",
681
+ );
682
+ await assertOriginalOrderOpen(orderId);
683
+ const { response, payload } = await invokeProviderWithRetry(
684
+ {
685
+ authorization: readiness.authorization,
686
+ orderId,
687
+ requestHash,
688
+ request: input.request,
689
+ },
690
+ context,
691
+ );
692
+ if (!response.ok || !payload?.ok)
693
+ throw Object.assign(
694
+ new Error(
695
+ payload?.error?.message || `Provider HTTP ${response.status}`,
696
+ ),
697
+ {
698
+ code: payload?.error?.code || "PROVIDER_FAILED",
699
+ status: response.status,
700
+ },
701
+ );
702
+ const delivered = providerOutput(
703
+ payload,
704
+ context,
705
+ input.request,
706
+ orderId,
707
+ requestHash,
708
+ );
709
+ await resultStore.savePending(invocation, delivered);
710
+ const output = delivered.output,
711
+ outputHash = delivered.outputHash;
712
+ const evidence = await waitForDeliveryEvidence(
713
+ orderId,
714
+ outputHash,
715
+ invocation.recoveryScope,
716
+ context,
717
+ requestHash,
718
+ );
719
+ if (String(evidence.usageUnits) !== String(delivered.usageUnits)) {
720
+ throw coded(
721
+ "DELIVERY_EVIDENCE_MISMATCH",
722
+ "Settlement Receipt usage does not match the Provider result",
723
+ );
724
+ }
725
+ const result = {
726
+ orderId,
727
+ requestHash,
728
+ version: "agentpay.runtime-result.v1",
729
+ service: { skillId: callSkillId, profileHash: callProfileHash },
730
+ output,
731
+ outputHash,
732
+ usageUnits: delivered.usageUnits,
733
+ deliveryEvidenceVerified: true,
734
+ confirmationRequired: false,
735
+ finalization: evidence.finalization,
736
+ deadlines: evidence.deadlines,
737
+ ...(includeProviderEvidence && payload.negativeProbe
738
+ ? { providerEvidence: payload.negativeProbe }
739
+ : {}),
740
+ };
741
+ await resultStore.save(
742
+ invocation,
743
+ normalizeRuntimeResult(result, context, requestHash, orderId),
744
+ );
745
+ return json(res, 200, { ok: true, result });
746
+ } catch (error) {
747
+ const status =
748
+ error?.status ||
749
+ ([
750
+ "FILE_INPUT_INVALID",
751
+ "INVALID_INPUT",
752
+ "INVALID_USAGE_CAP",
753
+ "SERVICE_REFERENCE_INVALID",
754
+ "INVALID_IDEMPOTENCY_KEY",
755
+ ].includes(error?.code)
756
+ ? 400
757
+ : [
758
+ "FILE_ORDER_UNAVAILABLE",
759
+ "FILE_RESULT_UNVERIFIED",
760
+ "QUOTE_TERMS_CHANGED",
761
+ "QUOTE_TERMS_CONFLICT",
762
+ "SERVICE_NOT_READY",
763
+ "INVOCATION_CONFLICT",
764
+ "INVOCATION_SERVICE_BINDING_MISMATCH",
765
+ "ORDER_CLOSING",
766
+ "ORDER_CANCELLED",
767
+ "ORDER_REFUNDED",
768
+ ].includes(error?.code)
769
+ ? 409
770
+ : 502);
771
+ return json(res, status, {
772
+ ok: false,
773
+ error: {
774
+ code: error?.code || "SKILL_INVOCATION_FAILED",
775
+ message: String(error?.message || error),
776
+ ...(error?.settlementCode
777
+ ? { settlementCode: error.settlementCode }
778
+ : {}),
779
+ ...(error?.detail ? { detail: error.detail } : {}),
780
+ },
781
+ });
782
+ } finally {
783
+ releaseInvocation();
784
+ }
785
+ });
786
+
787
+ server.listen(port, host, () => {
788
+ process.stdout.write(
789
+ `${JSON.stringify({
790
+ ready: true,
791
+ role: "user-runtime",
792
+ url: `http://${originHost}:${port}`,
793
+ })}\n`,
794
+ );
795
+ recovery.start();
796
+ });
797
+ for (const signal of ["SIGINT", "SIGTERM"])
798
+ process.once(signal, () => {
799
+ void recovery.stop();
800
+ server.close(() => process.exit(0));
801
+ });
802
+
803
+ async function assertOriginalOrderOpen(orderId) {
804
+ const result = settlementResult(
805
+ await callWallet("readSettlementOrder", { settlementUrl, orderId }),
806
+ );
807
+ if (result.order?.orderId !== orderId)
808
+ throw coded(
809
+ "SETTLEMENT_RESPONSE_INVALID",
810
+ "Settlement returned a different Order. Preserve the original request.",
811
+ );
812
+ assertOrderCanContinue(result);
813
+ if (result.order.vaultState === "Released")
814
+ throw Object.assign(
815
+ coded(
816
+ "ORDER_ALREADY_SETTLED",
817
+ "This Order is settled but no verified local result is available. Preserve its request; do not repeat the purchase.",
818
+ ),
819
+ { status: 409 },
820
+ );
821
+ return result;
822
+ }
823
+ async function waitForPaymentRequired(initial) {
824
+ const deadline = Date.now() + operationTimeoutMs;
825
+ let current = initial;
826
+ while (!current?.paymentRequired) {
827
+ assertOrderCanContinue(current);
828
+ if (Date.now() >= deadline)
829
+ throw coded(
830
+ "ORDER_PREPARATION_TIMEOUT",
831
+ "Vault creation did not finish in time",
832
+ );
833
+ await delay(Math.min(250, remaining(deadline)));
834
+ current = settlementResult(
835
+ await callWallet(
836
+ "readSettlementOrder",
837
+ { settlementUrl, orderId: current?.order?.orderId },
838
+ remaining(deadline),
839
+ ),
840
+ );
841
+ }
842
+ assertOrderCanContinue(current);
843
+ return current;
844
+ }
845
+ async function waitForServiceReady(orderId, requestHash) {
846
+ const deadline = Date.now() + operationTimeoutMs;
847
+ let last;
848
+ while (Date.now() < deadline) {
849
+ try {
850
+ return settlementResult(
851
+ await callWallet(
852
+ "requestSettlementServiceReady",
853
+ { settlementUrl, orderId, requestHash },
854
+ remaining(deadline),
855
+ ),
856
+ );
857
+ } catch (error) {
858
+ if (
859
+ [
860
+ "ORDER_CLOSING",
861
+ "ORDER_CANCELLED",
862
+ "ORDER_REFUNDED",
863
+ "ORDER_RESPONSE_DEADLINE_PASSED",
864
+ "ORDER_PAYMENT_DEADLINE_PASSED",
865
+ ].includes(error?.settlementCode || error?.code)
866
+ )
867
+ throw error;
868
+ await assertOriginalOrderOpen(orderId);
869
+ last = error;
870
+ await delay(Math.min(350, remaining(deadline)));
871
+ }
872
+ }
873
+ throw Object.assign(new Error("Payment was not activated in time"), {
874
+ code: "SERVICE_READY_TIMEOUT",
875
+ cause: last,
876
+ detail: last?.code
877
+ ? `${last.code}: ${last.message}`
878
+ : String(last?.message || last || "unknown"),
879
+ });
880
+ }
881
+ function delay(ms) {
882
+ return new Promise((resolve) => setTimeout(resolve, ms));
883
+ }
884
+ async function signCommitment(requirements, payable, commitmentNonce, context) {
885
+ const { skillId, profileHash } = context;
886
+ const validBefore =
887
+ Math.floor(Date.now() / 1000) + Number(requirements.maxTimeoutSeconds);
888
+ const signed = await callWallet("signAgentPayCommitment", {
889
+ commitment: {
890
+ version: "agentpay.safe-module.v1",
891
+ orderId: requirements.extra.orderId,
892
+ vault: requirements.payTo,
893
+ amount: requirements.amount,
894
+ termsHash: requirements.extra.termsHash,
895
+ skillId,
896
+ profileHash,
897
+ seller: payable.commitmentContext?.seller,
898
+ commitmentNonce,
899
+ validBefore,
900
+ },
901
+ });
902
+ return { ...signed, validBefore };
903
+ }
904
+ async function submitPayment(
905
+ paymentPayload,
906
+ orderId,
907
+ requestHash,
908
+ request,
909
+ context,
910
+ ) {
911
+ const { response, result } = await fetchJsonWithTimeout(
912
+ context.endpointUrl,
913
+ {
914
+ method: "POST",
915
+ headers: {
916
+ "content-type": "application/json",
917
+ "payment-signature": Buffer.from(
918
+ JSON.stringify(paymentPayload),
919
+ ).toString("base64"),
920
+ },
921
+ body: JSON.stringify(
922
+ providerRequest(context, orderId, requestHash, request),
923
+ ),
924
+ },
925
+ operationTimeoutMs,
926
+ );
927
+ if (![200, 202].includes(response.status))
928
+ throw Object.assign(
929
+ new Error(
930
+ result?.error?.message || `Provider payment HTTP ${response.status}`,
931
+ ),
932
+ {
933
+ code: result?.error?.code || "PAYMENT_FAILED",
934
+ status: response.status,
935
+ },
936
+ );
937
+ if (
938
+ !result?.result?.success &&
939
+ result?.result?.errorReason !== "settlement_pending"
940
+ )
941
+ throw Object.assign(
942
+ new Error(result?.result?.errorMessage || "Settlement rejected payment"),
943
+ { code: result?.result?.errorReason || "PAYMENT_REJECTED", status: 422 },
944
+ );
945
+ }
946
+ async function waitForDeliveryEvidence(
947
+ orderId,
948
+ outputHash,
949
+ recoveryScope,
950
+ context,
951
+ requestHash,
952
+ ) {
953
+ const deadline = Date.now() + operationTimeoutMs;
954
+ let last;
955
+ while (Date.now() < deadline) {
956
+ try {
957
+ const current = settlementResult(
958
+ await callWallet(
959
+ "readSettlementOrder",
960
+ {
961
+ settlementUrl,
962
+ orderId,
963
+ ...(recoveryScope ? { recoveryScope } : {}),
964
+ },
965
+ remaining(deadline),
966
+ ),
967
+ );
968
+ const o = current.order,
969
+ p = context.reference.profile;
970
+ if (
971
+ !o ||
972
+ o.orderId !== orderId ||
973
+ o.requestHash !== requestHash ||
974
+ o.skillId !== context.skillId ||
975
+ o.profileHash !== context.profileHash ||
976
+ o.providerId !== p.providerId ||
977
+ String(o.assetAddress).toLowerCase() !== p.asset ||
978
+ `eip155:${o.chainId}` !== p.network
979
+ )
980
+ throw coded(
981
+ "DELIVERY_EVIDENCE_MISMATCH",
982
+ "Settlement Order identity differs from the saved result",
983
+ );
984
+ const evidence = current.deliveryEvidence;
985
+ if (evidence) {
986
+ if (String(evidence.outputHash).toLowerCase() !== outputHash)
987
+ throw coded(
988
+ "DELIVERY_EVIDENCE_MISMATCH",
989
+ "Settlement Receipt does not match the returned output",
990
+ );
991
+ return {
992
+ ...evidence,
993
+ deadlines: current.deadlines,
994
+ finalization: readFinalizationDisclosure(current.finalization),
995
+ };
996
+ }
997
+ } catch (error) {
998
+ if (error?.code === "DELIVERY_EVIDENCE_MISMATCH") throw error;
999
+ last = error;
1000
+ }
1001
+ await delay(Math.min(250, remaining(deadline)));
1002
+ }
1003
+ throw Object.assign(
1004
+ new Error("Settlement did not expose accepted delivery evidence in time"),
1005
+ {
1006
+ code: "DELIVERY_EVIDENCE_TIMEOUT",
1007
+ detail: String(last?.message || last || "unknown"),
1008
+ },
1009
+ );
1010
+ }
1011
+ async function invokeProviderWithRetry(input, context) {
1012
+ const deadline = Date.now() + operationTimeoutMs;
1013
+ let last;
1014
+ let retryDelayMs = 200;
1015
+ const pause = async () => {
1016
+ await delay(Math.min(retryDelayMs, remaining(deadline)));
1017
+ retryDelayMs = Math.min(5000, retryDelayMs * 2);
1018
+ };
1019
+ while (Date.now() < deadline) {
1020
+ try {
1021
+ const { response, result: payload } = await fetchJsonWithTimeout(
1022
+ context.endpointUrl,
1023
+ {
1024
+ method: "POST",
1025
+ headers: {
1026
+ "content-type": "application/json",
1027
+ authorization: `AgentPay ${input.authorization}`,
1028
+ },
1029
+ body: JSON.stringify(
1030
+ providerRequest(
1031
+ context,
1032
+ input.orderId,
1033
+ input.requestHash,
1034
+ input.request,
1035
+ ),
1036
+ ),
1037
+ },
1038
+ remaining(deadline),
1039
+ );
1040
+ if (
1041
+ response.status === 502 &&
1042
+ payload?.error?.code === "PROVIDER_REJECTED"
1043
+ )
1044
+ return { response, payload };
1045
+ if (response.status >= 500) {
1046
+ last = Object.assign(
1047
+ new Error(
1048
+ payload?.error?.message || `Provider HTTP ${response.status}`,
1049
+ ),
1050
+ { code: payload?.error?.code || "PROVIDER_RETRY" },
1051
+ );
1052
+ await pause();
1053
+ continue;
1054
+ }
1055
+ return { response, payload };
1056
+ } catch (error) {
1057
+ if (
1058
+ [
1059
+ "TRANSPORT_REDIRECT_REJECTED",
1060
+ "TRANSPORT_RESPONSE_TOO_LARGE",
1061
+ ].includes(error?.code)
1062
+ )
1063
+ throw error;
1064
+ last = error;
1065
+ await pause();
1066
+ }
1067
+ }
1068
+ throw Object.assign(
1069
+ new Error("Provider did not recover before the invocation deadline"),
1070
+ {
1071
+ code: "PROVIDER_RECOVERY_TIMEOUT",
1072
+ detail: String(last?.message || last || "unknown"),
1073
+ },
1074
+ );
1075
+ }
1076
+
1077
+ async function callWallet(method, params, timeoutMs = operationTimeoutMs) {
1078
+ const handle = await open(
1079
+ walletCapabilityFile,
1080
+ constants.O_RDONLY | constants.O_NOFOLLOW,
1081
+ );
1082
+ let walletCapability;
1083
+ try {
1084
+ const info = await handle.stat();
1085
+ if (
1086
+ !info.isFile() ||
1087
+ info.uid !== process.getuid() ||
1088
+ info.mode & 0o077 ||
1089
+ info.size > 1024
1090
+ )
1091
+ throw coded(
1092
+ "WALLET_CREDENTIAL_UNAVAILABLE",
1093
+ "Check local Wallet installation.",
1094
+ );
1095
+ walletCapability = (await handle.readFile("utf8")).trim();
1096
+ } finally {
1097
+ await handle.close();
1098
+ }
1099
+ if (
1100
+ !/^[0-9a-f]{64}$/.test(walletCapability) ||
1101
+ walletCapability === callerCredential
1102
+ )
1103
+ throw coded(
1104
+ "WALLET_CREDENTIAL_UNAVAILABLE",
1105
+ "Check local Wallet installation.",
1106
+ );
1107
+ const request = `${JSON.stringify({ id: randomBytes(8).toString("hex"), token: walletCapability, method, params })}\n`;
1108
+ return new Promise((resolve, reject) => {
1109
+ const socket = createConnection(walletSocket);
1110
+ let response = "";
1111
+ socket.setEncoding("utf8");
1112
+ let settled = false;
1113
+ const finish = (error, value) => {
1114
+ if (settled) return;
1115
+ settled = true;
1116
+ clearTimeout(timer);
1117
+ socket.destroy();
1118
+ if (error) reject(error);
1119
+ else resolve(value);
1120
+ };
1121
+ const timer = setTimeout(
1122
+ () =>
1123
+ finish(coded("WALLET_TIMEOUT", "Wallet IPC did not respond in time")),
1124
+ Math.max(1, timeoutMs),
1125
+ );
1126
+ socket.once("error", (error) => finish(error));
1127
+ socket.once("close", () =>
1128
+ finish(
1129
+ coded(
1130
+ "WALLET_EMPTY_RESPONSE",
1131
+ "Wallet IPC closed without a complete response",
1132
+ ),
1133
+ ),
1134
+ );
1135
+ socket.on("data", (chunk) => {
1136
+ response += chunk;
1137
+ if (response.length > 2 * 1048576)
1138
+ finish(
1139
+ coded(
1140
+ "WALLET_RESPONSE_TOO_LARGE",
1141
+ "Wallet response exceeds the size limit",
1142
+ ),
1143
+ );
1144
+ });
1145
+ socket.on("end", () => {
1146
+ try {
1147
+ if (!response.trim())
1148
+ throw coded(
1149
+ "WALLET_EMPTY_RESPONSE",
1150
+ "Wallet IPC closed without a response",
1151
+ );
1152
+ const payload = JSON.parse(response);
1153
+ if (!payload.ok) throw walletIpcError(payload);
1154
+ finish(null, payload.result);
1155
+ } catch (error) {
1156
+ finish(error);
1157
+ }
1158
+ });
1159
+ socket.write(request);
1160
+ });
1161
+ }
1162
+ function remaining(deadline) {
1163
+ return Math.max(1, deadline - Date.now());
1164
+ }
1165
+ async function readJson(req) {
1166
+ const chunks = [];
1167
+ let length = 0;
1168
+ for await (const chunk of req) {
1169
+ length += chunk.length;
1170
+ if (length > 300000)
1171
+ throw coded(
1172
+ "INVALID_INPUT",
1173
+ "Request body exceeds the local JSON size limit",
1174
+ );
1175
+ chunks.push(chunk);
1176
+ }
1177
+ try {
1178
+ return JSON.parse(Buffer.concat(chunks).toString("utf8") || "{}");
1179
+ } catch {
1180
+ throw coded("INVALID_INPUT", "invalid JSON body");
1181
+ }
1182
+ }
1183
+ function json(res, status, body) {
1184
+ res.writeHead(status, {
1185
+ "content-type": "application/json",
1186
+ "cache-control": "no-store",
1187
+ });
1188
+ res.end(JSON.stringify(body));
1189
+ }
1190
+ function requiredEnv(name) {
1191
+ const value = String(process.env[name] || "").trim();
1192
+ if (!value) throw new Error(`${name} is required`);
1193
+ return value;
1194
+ }
1195
+ function bytes32(value, name) {
1196
+ const text = String(value || "").toLowerCase();
1197
+ if (!/^0x[0-9a-f]{64}$/.test(text))
1198
+ throw new Error(`${name} must be bytes32`);
1199
+ return text;
1200
+ }
1201
+ function uint(value, name) {
1202
+ const text = String(value || "");
1203
+ if (!/^[1-9][0-9]*$/.test(text))
1204
+ throw new Error(`${name} must be a positive uint`);
1205
+ return text;
1206
+ }
1207
+ function coded(code, message) {
1208
+ return Object.assign(new Error(message), { code });
1209
+ }
1210
+ async function acquireInvocation(value) {
1211
+ const key = String(value || "");
1212
+ if (activeInvocations >= 64)
1213
+ throw Object.assign(
1214
+ coded("INVOCATION_BUSY", "Runtime is busy; retry the same request later"),
1215
+ { status: 503 },
1216
+ );
1217
+ activeInvocations++;
1218
+ const previous = invocationLocks.get(key) || Promise.resolve();
1219
+ let unlock;
1220
+ const current = new Promise((resolve) => {
1221
+ unlock = resolve;
1222
+ });
1223
+ invocationLocks.set(key, current);
1224
+ await previous;
1225
+ let released = false;
1226
+ return () => {
1227
+ if (released) return;
1228
+ released = true;
1229
+ activeInvocations--;
1230
+ unlock();
1231
+ if (invocationLocks.get(key) === current) invocationLocks.delete(key);
1232
+ };
1233
+ }