arc402-cli 1.0.0-rc.1 → 1.1.0

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 (257) hide show
  1. package/README.md +43 -2
  2. package/dist/abis.d.ts +1 -0
  3. package/dist/abis.d.ts.map +1 -1
  4. package/dist/abis.js +29 -1
  5. package/dist/abis.js.map +1 -1
  6. package/dist/commands/backup.d.ts +3 -0
  7. package/dist/commands/backup.d.ts.map +1 -0
  8. package/dist/commands/backup.js +106 -0
  9. package/dist/commands/backup.js.map +1 -0
  10. package/dist/commands/compute.d.ts +14 -0
  11. package/dist/commands/compute.d.ts.map +1 -0
  12. package/dist/commands/compute.js +466 -0
  13. package/dist/commands/compute.js.map +1 -0
  14. package/dist/commands/config.d.ts.map +1 -1
  15. package/dist/commands/config.js +11 -1
  16. package/dist/commands/config.js.map +1 -1
  17. package/dist/commands/daemon.d.ts.map +1 -1
  18. package/dist/commands/daemon.js +67 -0
  19. package/dist/commands/daemon.js.map +1 -1
  20. package/dist/commands/discover.d.ts.map +1 -1
  21. package/dist/commands/discover.js +60 -15
  22. package/dist/commands/discover.js.map +1 -1
  23. package/dist/commands/doctor.d.ts +3 -0
  24. package/dist/commands/doctor.d.ts.map +1 -0
  25. package/dist/commands/doctor.js +205 -0
  26. package/dist/commands/doctor.js.map +1 -0
  27. package/dist/commands/tunnel.d.ts +3 -0
  28. package/dist/commands/tunnel.d.ts.map +1 -0
  29. package/dist/commands/tunnel.js +281 -0
  30. package/dist/commands/tunnel.js.map +1 -0
  31. package/dist/commands/wallet.d.ts.map +1 -1
  32. package/dist/commands/wallet.js +299 -65
  33. package/dist/commands/wallet.js.map +1 -1
  34. package/dist/commands/watch.d.ts.map +1 -1
  35. package/dist/commands/watch.js +146 -9
  36. package/dist/commands/watch.js.map +1 -1
  37. package/dist/commands/workroom.d.ts.map +1 -1
  38. package/dist/commands/workroom.js +112 -6
  39. package/dist/commands/workroom.js.map +1 -1
  40. package/dist/config.d.ts +13 -0
  41. package/dist/config.d.ts.map +1 -1
  42. package/dist/config.js +41 -4
  43. package/dist/config.js.map +1 -1
  44. package/dist/daemon/compute-metering.d.ts +61 -0
  45. package/dist/daemon/compute-metering.d.ts.map +1 -0
  46. package/dist/daemon/compute-metering.js +299 -0
  47. package/dist/daemon/compute-metering.js.map +1 -0
  48. package/dist/daemon/compute-session.d.ts +100 -0
  49. package/dist/daemon/compute-session.d.ts.map +1 -0
  50. package/dist/daemon/compute-session.js +231 -0
  51. package/dist/daemon/compute-session.js.map +1 -0
  52. package/dist/daemon/config.d.ts +33 -1
  53. package/dist/daemon/config.d.ts.map +1 -1
  54. package/dist/daemon/config.js +69 -0
  55. package/dist/daemon/config.js.map +1 -1
  56. package/dist/daemon/credentials.d.ts +24 -0
  57. package/dist/daemon/credentials.d.ts.map +1 -0
  58. package/dist/daemon/credentials.js +80 -0
  59. package/dist/daemon/credentials.js.map +1 -0
  60. package/dist/daemon/delivery-client.d.ts +35 -0
  61. package/dist/daemon/delivery-client.d.ts.map +1 -0
  62. package/dist/daemon/delivery-client.js +231 -0
  63. package/dist/daemon/delivery-client.js.map +1 -0
  64. package/dist/daemon/file-delivery.d.ts +98 -0
  65. package/dist/daemon/file-delivery.d.ts.map +1 -0
  66. package/dist/daemon/file-delivery.js +461 -0
  67. package/dist/daemon/file-delivery.js.map +1 -0
  68. package/dist/daemon/index.d.ts +1 -0
  69. package/dist/daemon/index.d.ts.map +1 -1
  70. package/dist/daemon/index.js +793 -227
  71. package/dist/daemon/index.js.map +1 -1
  72. package/dist/daemon/notify.d.ts +35 -6
  73. package/dist/daemon/notify.d.ts.map +1 -1
  74. package/dist/daemon/notify.js +176 -48
  75. package/dist/daemon/notify.js.map +1 -1
  76. package/dist/daemon/worker-executor.d.ts +71 -0
  77. package/dist/daemon/worker-executor.d.ts.map +1 -0
  78. package/dist/daemon/worker-executor.js +382 -0
  79. package/dist/daemon/worker-executor.js.map +1 -0
  80. package/dist/drain-v4.js +2 -2
  81. package/dist/drain-v4.js.map +1 -1
  82. package/dist/endpoint-notify.d.ts +9 -1
  83. package/dist/endpoint-notify.d.ts.map +1 -1
  84. package/dist/endpoint-notify.js +116 -3
  85. package/dist/endpoint-notify.js.map +1 -1
  86. package/dist/index.js +81 -1
  87. package/dist/index.js.map +1 -1
  88. package/dist/program.d.ts.map +1 -1
  89. package/dist/program.js +8 -0
  90. package/dist/program.js.map +1 -1
  91. package/dist/repl.d.ts.map +1 -1
  92. package/dist/repl.js +69 -486
  93. package/dist/repl.js.map +1 -1
  94. package/dist/tui/App.d.ts +12 -0
  95. package/dist/tui/App.d.ts.map +1 -0
  96. package/dist/tui/App.js +154 -0
  97. package/dist/tui/App.js.map +1 -0
  98. package/dist/tui/Footer.d.ts +11 -0
  99. package/dist/tui/Footer.d.ts.map +1 -0
  100. package/dist/tui/Footer.js +13 -0
  101. package/dist/tui/Footer.js.map +1 -0
  102. package/dist/tui/Header.d.ts +14 -0
  103. package/dist/tui/Header.d.ts.map +1 -0
  104. package/dist/tui/Header.js +19 -0
  105. package/dist/tui/Header.js.map +1 -0
  106. package/dist/tui/InputLine.d.ts +11 -0
  107. package/dist/tui/InputLine.d.ts.map +1 -0
  108. package/dist/tui/InputLine.js +145 -0
  109. package/dist/tui/InputLine.js.map +1 -0
  110. package/dist/tui/Viewport.d.ts +14 -0
  111. package/dist/tui/Viewport.d.ts.map +1 -0
  112. package/dist/tui/Viewport.js +48 -0
  113. package/dist/tui/Viewport.js.map +1 -0
  114. package/dist/tui/WalletConnectPairing.d.ts +23 -0
  115. package/dist/tui/WalletConnectPairing.d.ts.map +1 -0
  116. package/dist/tui/WalletConnectPairing.js +61 -0
  117. package/dist/tui/WalletConnectPairing.js.map +1 -0
  118. package/dist/tui/components/Button.d.ts +7 -0
  119. package/dist/tui/components/Button.d.ts.map +1 -0
  120. package/dist/tui/components/Button.js +21 -0
  121. package/dist/tui/components/Button.js.map +1 -0
  122. package/dist/tui/components/CeremonyView.d.ts +13 -0
  123. package/dist/tui/components/CeremonyView.d.ts.map +1 -0
  124. package/dist/tui/components/CeremonyView.js +10 -0
  125. package/dist/tui/components/CeremonyView.js.map +1 -0
  126. package/dist/tui/components/CompletionDropdown.d.ts +7 -0
  127. package/dist/tui/components/CompletionDropdown.d.ts.map +1 -0
  128. package/dist/tui/components/CompletionDropdown.js +23 -0
  129. package/dist/tui/components/CompletionDropdown.js.map +1 -0
  130. package/dist/tui/components/ConfirmPrompt.d.ts +9 -0
  131. package/dist/tui/components/ConfirmPrompt.d.ts.map +1 -0
  132. package/dist/tui/components/ConfirmPrompt.js +10 -0
  133. package/dist/tui/components/ConfirmPrompt.js.map +1 -0
  134. package/dist/tui/components/CustomTextInput.d.ts +15 -0
  135. package/dist/tui/components/CustomTextInput.d.ts.map +1 -0
  136. package/dist/tui/components/CustomTextInput.js +99 -0
  137. package/dist/tui/components/CustomTextInput.js.map +1 -0
  138. package/dist/tui/components/InteractiveTable.d.ts +14 -0
  139. package/dist/tui/components/InteractiveTable.d.ts.map +1 -0
  140. package/dist/tui/components/InteractiveTable.js +61 -0
  141. package/dist/tui/components/InteractiveTable.js.map +1 -0
  142. package/dist/tui/components/StepSpinner.d.ts +11 -0
  143. package/dist/tui/components/StepSpinner.d.ts.map +1 -0
  144. package/dist/tui/components/StepSpinner.js +32 -0
  145. package/dist/tui/components/StepSpinner.js.map +1 -0
  146. package/dist/tui/components/Toast.d.ts +18 -0
  147. package/dist/tui/components/Toast.d.ts.map +1 -0
  148. package/dist/tui/components/Toast.js +29 -0
  149. package/dist/tui/components/Toast.js.map +1 -0
  150. package/dist/tui/index.d.ts +2 -0
  151. package/dist/tui/index.d.ts.map +1 -0
  152. package/dist/tui/index.js +55 -0
  153. package/dist/tui/index.js.map +1 -0
  154. package/dist/tui/useChat.d.ts +11 -0
  155. package/dist/tui/useChat.d.ts.map +1 -0
  156. package/dist/tui/useChat.js +91 -0
  157. package/dist/tui/useChat.js.map +1 -0
  158. package/dist/tui/useCommand.d.ts +12 -0
  159. package/dist/tui/useCommand.d.ts.map +1 -0
  160. package/dist/tui/useCommand.js +137 -0
  161. package/dist/tui/useCommand.js.map +1 -0
  162. package/dist/tui/useNotifications.d.ts +9 -0
  163. package/dist/tui/useNotifications.d.ts.map +1 -0
  164. package/dist/tui/useNotifications.js +17 -0
  165. package/dist/tui/useNotifications.js.map +1 -0
  166. package/dist/tui/useScroll.d.ts +17 -0
  167. package/dist/tui/useScroll.d.ts.map +1 -0
  168. package/dist/tui/useScroll.js +46 -0
  169. package/dist/tui/useScroll.js.map +1 -0
  170. package/dist/ui/format.d.ts.map +1 -1
  171. package/dist/ui/format.js +2 -0
  172. package/dist/ui/format.js.map +1 -1
  173. package/dist/ui/qr-render.d.ts +25 -0
  174. package/dist/ui/qr-render.d.ts.map +1 -0
  175. package/dist/ui/qr-render.js +90 -0
  176. package/dist/ui/qr-render.js.map +1 -0
  177. package/dist/ui/rpc-fallback.d.ts +11 -0
  178. package/dist/ui/rpc-fallback.d.ts.map +1 -0
  179. package/dist/ui/rpc-fallback.js +58 -0
  180. package/dist/ui/rpc-fallback.js.map +1 -0
  181. package/dist/walletconnect.d.ts +4 -0
  182. package/dist/walletconnect.d.ts.map +1 -1
  183. package/dist/walletconnect.js.map +1 -1
  184. package/package.json +11 -3
  185. package/scripts/authorize-machine-key.ts +0 -43
  186. package/scripts/drain-wallet.ts +0 -149
  187. package/scripts/execute-spend-only.ts +0 -81
  188. package/scripts/register-agent-userop.ts +0 -186
  189. package/src/abis.ts +0 -187
  190. package/src/bundler.ts +0 -235
  191. package/src/client.ts +0 -36
  192. package/src/coinbase-smart-wallet.ts +0 -51
  193. package/src/commands/accept.ts +0 -64
  194. package/src/commands/agent-handshake.ts +0 -72
  195. package/src/commands/agent.ts +0 -691
  196. package/src/commands/agreements.ts +0 -350
  197. package/src/commands/arbitrator.ts +0 -180
  198. package/src/commands/arena-handshake.ts +0 -257
  199. package/src/commands/arena.ts +0 -122
  200. package/src/commands/cancel.ts +0 -35
  201. package/src/commands/channel.ts +0 -218
  202. package/src/commands/coldstart.ts +0 -165
  203. package/src/commands/config.ts +0 -58
  204. package/src/commands/contract-interaction.ts +0 -166
  205. package/src/commands/daemon.ts +0 -978
  206. package/src/commands/deliver.ts +0 -148
  207. package/src/commands/discover.ts +0 -297
  208. package/src/commands/dispute.ts +0 -375
  209. package/src/commands/endpoint.ts +0 -620
  210. package/src/commands/feed.ts +0 -229
  211. package/src/commands/hire.ts +0 -245
  212. package/src/commands/migrate.ts +0 -177
  213. package/src/commands/negotiate.ts +0 -271
  214. package/src/commands/openshell.ts +0 -1055
  215. package/src/commands/owner.ts +0 -35
  216. package/src/commands/policy.ts +0 -263
  217. package/src/commands/relay.ts +0 -273
  218. package/src/commands/remediate.ts +0 -24
  219. package/src/commands/reputation.ts +0 -79
  220. package/src/commands/setup.ts +0 -343
  221. package/src/commands/trust.ts +0 -27
  222. package/src/commands/verify.ts +0 -91
  223. package/src/commands/wallet.ts +0 -3280
  224. package/src/commands/watch.ts +0 -23
  225. package/src/commands/watchtower.ts +0 -248
  226. package/src/commands/workroom.ts +0 -959
  227. package/src/config.ts +0 -174
  228. package/src/daemon/config.ts +0 -308
  229. package/src/daemon/hire-listener.ts +0 -226
  230. package/src/daemon/index.ts +0 -955
  231. package/src/daemon/job-lifecycle.ts +0 -215
  232. package/src/daemon/notify.ts +0 -157
  233. package/src/daemon/token-metering.ts +0 -183
  234. package/src/daemon/userops.ts +0 -119
  235. package/src/daemon/wallet-monitor.ts +0 -90
  236. package/src/drain-v4.ts +0 -159
  237. package/src/endpoint-config.ts +0 -83
  238. package/src/endpoint-notify.ts +0 -46
  239. package/src/index.ts +0 -26
  240. package/src/openshell-runtime.ts +0 -277
  241. package/src/program.ts +0 -83
  242. package/src/repl.ts +0 -680
  243. package/src/signing.ts +0 -28
  244. package/src/telegram-notify.ts +0 -88
  245. package/src/ui/banner.ts +0 -51
  246. package/src/ui/colors.ts +0 -30
  247. package/src/ui/format.ts +0 -77
  248. package/src/ui/spinner.ts +0 -56
  249. package/src/ui/tree.ts +0 -16
  250. package/src/utils/format.ts +0 -48
  251. package/src/utils/hash.ts +0 -5
  252. package/src/utils/time.ts +0 -15
  253. package/src/wallet-router.ts +0 -178
  254. package/src/walletconnect-session.ts +0 -27
  255. package/src/walletconnect.ts +0 -294
  256. package/test/time.test.js +0 -11
  257. package/tsconfig.json +0 -19
@@ -52,12 +52,18 @@ const net = __importStar(require("net"));
52
52
  const http = __importStar(require("http"));
53
53
  const ethers_1 = require("ethers");
54
54
  const better_sqlite3_1 = __importDefault(require("better-sqlite3"));
55
+ const crypto = __importStar(require("crypto"));
55
56
  const config_1 = require("./config");
57
+ const compute_metering_1 = require("./compute-metering");
58
+ const compute_session_1 = require("./compute-session");
56
59
  const wallet_monitor_1 = require("./wallet-monitor");
57
60
  const notify_1 = require("./notify");
58
61
  const hire_listener_1 = require("./hire-listener");
59
62
  const userops_1 = require("./userops");
60
63
  const job_lifecycle_1 = require("./job-lifecycle");
64
+ const file_delivery_1 = require("./file-delivery");
65
+ const delivery_client_1 = require("./delivery-client");
66
+ const abis_1 = require("../abis");
61
67
  function openStateDB(dbPath) {
62
68
  const db = new better_sqlite3_1.default(dbPath);
63
69
  db.pragma("journal_mode = WAL");
@@ -114,6 +120,7 @@ function openStateDB(dbPath) {
114
120
  (@id, @agreement_id, @hirer_address, @capability, @price_eth, @deadline_unix, @spec_hash, @status, @created_at, @updated_at, @reject_reason)
115
121
  `);
116
122
  const getHireRequest = db.prepare(`SELECT * FROM hire_requests WHERE id = ?`);
123
+ const getHireRequestByAgreementId = db.prepare(`SELECT * FROM hire_requests WHERE agreement_id = ? ORDER BY created_at DESC LIMIT 1`);
117
124
  const updateStatus = db.prepare(`UPDATE hire_requests SET status = ?, reject_reason = ?, updated_at = ? WHERE id = ?`);
118
125
  const listPending = db.prepare(`SELECT * FROM hire_requests WHERE status = 'pending_approval' ORDER BY created_at ASC`);
119
126
  const listActive = db.prepare(`SELECT * FROM hire_requests WHERE status IN ('accepted', 'delivered') ORDER BY created_at ASC`);
@@ -126,6 +133,9 @@ function openStateDB(dbPath) {
126
133
  getHireRequest(id) {
127
134
  return getHireRequest.get(id);
128
135
  },
136
+ getHireRequestByAgreementId(agreementId) {
137
+ return getHireRequestByAgreementId.get(agreementId);
138
+ },
129
139
  updateHireRequestStatus(id, status, rejectReason) {
130
140
  updateStatus.run(status, rejectReason ?? null, Date.now(), id);
131
141
  },
@@ -144,6 +154,40 @@ function openStateDB(dbPath) {
144
154
  },
145
155
  };
146
156
  }
157
+ // ─── Auth token ───────────────────────────────────────────────────────────────
158
+ const DAEMON_TOKEN_FILE = path.join(path.dirname(config_1.DAEMON_SOCK), "daemon.token");
159
+ function generateApiToken() {
160
+ return crypto.randomBytes(32).toString("hex");
161
+ }
162
+ function saveApiToken(token) {
163
+ fs.mkdirSync(path.dirname(DAEMON_TOKEN_FILE), { recursive: true, mode: 0o700 });
164
+ fs.writeFileSync(DAEMON_TOKEN_FILE, token, { mode: 0o600 });
165
+ }
166
+ function loadApiToken() {
167
+ try {
168
+ return fs.readFileSync(DAEMON_TOKEN_FILE, "utf-8").trim();
169
+ }
170
+ catch {
171
+ return null;
172
+ }
173
+ }
174
+ const rateLimitMap = new Map();
175
+ const RATE_LIMIT = 30;
176
+ const RATE_WINDOW_MS = 60000;
177
+ function checkRateLimit(ip) {
178
+ const now = Date.now();
179
+ let bucket = rateLimitMap.get(ip);
180
+ if (!bucket || now >= bucket.resetTime) {
181
+ bucket = { count: 0, resetTime: now + RATE_WINDOW_MS };
182
+ rateLimitMap.set(ip, bucket);
183
+ }
184
+ bucket.count++;
185
+ return bucket.count <= RATE_LIMIT;
186
+ }
187
+ // Cleanup stale rate limit entries every 5 minutes to prevent unbounded growth
188
+ let rateLimitCleanupInterval = null;
189
+ // ─── Body size limit ──────────────────────────────────────────────────────────
190
+ const MAX_BODY_SIZE = 1024 * 1024; // 1 MB
147
191
  // ─── Logger ───────────────────────────────────────────────────────────────────
148
192
  function openLogger(logPath, foreground) {
149
193
  let stream = null;
@@ -161,13 +205,14 @@ function openLogger(logPath, foreground) {
161
205
  }
162
206
  };
163
207
  }
164
- function startIpcServer(ctx, log) {
208
+ function startIpcServer(ctx, log, apiToken) {
165
209
  // Remove stale socket
166
210
  if (fs.existsSync(config_1.DAEMON_SOCK)) {
167
211
  fs.unlinkSync(config_1.DAEMON_SOCK);
168
212
  }
169
213
  const server = net.createServer((socket) => {
170
214
  let buf = "";
215
+ let authenticated = false;
171
216
  socket.on("data", (data) => {
172
217
  buf += data.toString();
173
218
  const lines = buf.split("\n");
@@ -183,6 +228,19 @@ function startIpcServer(ctx, log) {
183
228
  socket.write(JSON.stringify({ ok: false, error: "invalid_json" }) + "\n");
184
229
  continue;
185
230
  }
231
+ // First message must be auth
232
+ if (!authenticated) {
233
+ if (cmd.auth === apiToken) {
234
+ authenticated = true;
235
+ socket.write(JSON.stringify({ ok: true, authenticated: true }) + "\n");
236
+ }
237
+ else {
238
+ log({ event: "ipc_auth_failed" });
239
+ socket.write(JSON.stringify({ ok: false, error: "unauthorized" }) + "\n");
240
+ socket.destroy();
241
+ }
242
+ continue;
243
+ }
186
244
  const response = handleIpcCommand(cmd, ctx, log);
187
245
  socket.write(JSON.stringify(response) + "\n");
188
246
  }
@@ -404,15 +462,34 @@ async function runDaemon(foreground = false) {
404
462
  process.exit(1);
405
463
  }
406
464
  // ── Setup notifier ───────────────────────────────────────────────────────
407
- const notifier = new notify_1.Notifier(config.notifications.telegram_bot_token, config.notifications.telegram_chat_id, {
408
- hire_request: config.notifications.notify_on_hire_request,
409
- hire_accepted: config.notifications.notify_on_hire_accepted,
410
- hire_rejected: config.notifications.notify_on_hire_rejected,
411
- delivery: config.notifications.notify_on_delivery,
412
- dispute: config.notifications.notify_on_dispute,
413
- channel_challenge: config.notifications.notify_on_channel_challenge,
414
- low_balance: config.notifications.notify_on_low_balance,
465
+ const notifier = (0, notify_1.buildNotifier)(config);
466
+ // ── File delivery subsystem ──────────────────────────────────────────────
467
+ const fileDelivery = new file_delivery_1.FileDeliveryManager({
468
+ maxFileSizeMb: config.delivery.max_file_size_mb,
469
+ maxJobSizeMb: config.delivery.max_job_size_mb,
415
470
  });
471
+ fileDelivery.setPartyResolver((agreementId) => {
472
+ const row = db.getHireRequestByAgreementId(agreementId);
473
+ if (!row)
474
+ return null;
475
+ return {
476
+ hirerAddress: row.hirer_address,
477
+ providerAddress: config.wallet.contract_address,
478
+ };
479
+ });
480
+ const deliveryClient = new delivery_client_1.DeliveryClient({ autoDownload: config.delivery.auto_download });
481
+ deliveryClient.log = log;
482
+ log({ event: "file_delivery_ready", serve_files: config.delivery.serve_files, auto_download: config.delivery.auto_download });
483
+ // ── Compute rental subsystem ─────────────────────────────────────────────
484
+ let computeMetering = null;
485
+ let computeSessions = null;
486
+ // Machine key signer — used for on-chain compute contract calls
487
+ const machineKeySigner = new ethers_1.ethers.Wallet(machinePrivateKey, provider);
488
+ if (config.compute.enabled) {
489
+ computeMetering = new compute_metering_1.ComputeMetering(machinePrivateKey, config.network.chain_id, config.compute.compute_agreement_address || config.wallet.contract_address, config.compute.metering_interval_seconds, config.compute.report_interval_minutes, config.compute.compute_agreement_address ? provider : undefined);
490
+ computeSessions = new compute_session_1.ComputeSessionManager(computeMetering);
491
+ log({ event: "compute_enabled", gpu_spec: config.compute.gpu_spec, rate_wei: config.compute.rate_per_hour_wei });
492
+ }
416
493
  // ── Step 10: Start relay listener ───────────────────────────────────────
417
494
  const hireListener = new hire_listener_1.HireListener(config, db, notifier, config.wallet.contract_address);
418
495
  const ipcCtx = {
@@ -464,6 +541,14 @@ async function runDaemon(foreground = false) {
464
541
  }
465
542
  }
466
543
  }, 30000);
544
+ // Rate limit map cleanup — every 5 minutes (prevents unbounded growth)
545
+ rateLimitCleanupInterval = setInterval(() => {
546
+ const now = Date.now();
547
+ for (const [ip, bucket] of rateLimitMap) {
548
+ if (bucket.resetTime < now)
549
+ rateLimitMap.delete(ip);
550
+ }
551
+ }, 5 * 60 * 1000);
467
552
  // Balance monitor — every 5 minutes
468
553
  const balanceInterval = setInterval(async () => {
469
554
  try {
@@ -481,15 +566,76 @@ async function runDaemon(foreground = false) {
481
566
  fs.writeFileSync(config_1.DAEMON_PID, String(process.pid), { mode: 0o600 });
482
567
  log({ event: "pid_written", pid: process.pid, path: config_1.DAEMON_PID });
483
568
  }
569
+ // ── Generate and save API token ──────────────────────────────────────────
570
+ const apiToken = generateApiToken();
571
+ saveApiToken(apiToken);
572
+ log({ event: "auth_token_saved", path: DAEMON_TOKEN_FILE });
484
573
  // ── Start IPC socket ─────────────────────────────────────────────────────
485
- const ipcServer = startIpcServer(ipcCtx, log);
574
+ const ipcServer = startIpcServer(ipcCtx, log, apiToken);
486
575
  // ── Start HTTP relay server (public endpoint) ────────────────────────────
487
576
  const httpPort = config.relay.listen_port ?? 4402;
577
+ /**
578
+ * Optionally verifies X-ARC402-Signature against the request body.
579
+ * Logs the result but never rejects — unsigned requests are accepted for backwards compat.
580
+ */
581
+ function verifyRequestSignature(body, req) {
582
+ const sig = req.headers["x-arc402-signature"];
583
+ if (!sig)
584
+ return;
585
+ const claimedSigner = req.headers["x-arc402-signer"];
586
+ try {
587
+ const recovered = ethers_1.ethers.verifyMessage(body, sig);
588
+ if (claimedSigner && recovered.toLowerCase() !== claimedSigner.toLowerCase()) {
589
+ log({ event: "sig_mismatch", claimed: claimedSigner, recovered });
590
+ }
591
+ else {
592
+ log({ event: "sig_verified", signer: recovered });
593
+ }
594
+ }
595
+ catch {
596
+ log({ event: "sig_invalid" });
597
+ }
598
+ }
599
+ /**
600
+ * Read request body with a size cap. Destroys the request and sends 413
601
+ * if the body exceeds MAX_BODY_SIZE. Returns null in that case.
602
+ */
603
+ function readBody(req, res) {
604
+ return new Promise((resolve) => {
605
+ let body = "";
606
+ let size = 0;
607
+ req.on("data", (chunk) => {
608
+ size += chunk.length;
609
+ if (size > MAX_BODY_SIZE) {
610
+ req.destroy();
611
+ res.writeHead(413, { "Content-Type": "application/json" });
612
+ res.end(JSON.stringify({ error: "payload_too_large" }));
613
+ resolve(null);
614
+ return;
615
+ }
616
+ body += chunk.toString();
617
+ });
618
+ req.on("end", () => { resolve(body); });
619
+ req.on("error", () => { resolve(null); });
620
+ });
621
+ }
622
+ const PUBLIC_GET_PATHS = new Set(["/", "/health", "/agent", "/capabilities", "/status"]);
623
+ // CORS whitelist — localhost for local tooling, arc402.xyz for the web app
624
+ const CORS_WHITELIST = new Set(["localhost", "127.0.0.1", "arc402.xyz", "app.arc402.xyz"]);
488
625
  const httpServer = http.createServer(async (req, res) => {
489
- // CORS headers
490
- res.setHeader("Access-Control-Allow-Origin", "*");
491
- res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
492
- res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
626
+ // CORS — only reflect origin header if it's in the whitelist
627
+ const origin = (req.headers["origin"] ?? "");
628
+ if (origin) {
629
+ try {
630
+ const { hostname } = new URL(origin);
631
+ if (CORS_WHITELIST.has(hostname)) {
632
+ res.setHeader("Access-Control-Allow-Origin", origin);
633
+ res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
634
+ res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
635
+ }
636
+ }
637
+ catch { /* ignore invalid origin */ }
638
+ }
493
639
  if (req.method === "OPTIONS") {
494
640
  res.writeHead(204);
495
641
  res.end();
@@ -497,6 +643,26 @@ async function runDaemon(foreground = false) {
497
643
  }
498
644
  const url = new URL(req.url || "/", `http://localhost:${httpPort}`);
499
645
  const pathname = url.pathname;
646
+ // Rate limiting (all endpoints)
647
+ const clientIp = (req.socket.remoteAddress ?? "unknown").replace(/^::ffff:/, "");
648
+ if (!checkRateLimit(clientIp)) {
649
+ log({ event: "rate_limited", ip: clientIp, path: pathname });
650
+ res.writeHead(429, { "Content-Type": "application/json" });
651
+ res.end(JSON.stringify({ error: "too_many_requests" }));
652
+ return;
653
+ }
654
+ // Auth required on all POST endpoints (GET public paths are open).
655
+ // /job/* GET routes use party auth (verifyPartyAccess) instead of daemon bearer token.
656
+ if (req.method === "POST" || (req.method === "GET" && !PUBLIC_GET_PATHS.has(pathname) && !pathname.startsWith("/job/"))) {
657
+ const authHeader = req.headers["authorization"] ?? "";
658
+ const token = authHeader.startsWith("Bearer ") ? authHeader.slice(7) : "";
659
+ if (token !== apiToken) {
660
+ log({ event: "http_unauthorized", ip: clientIp, path: pathname });
661
+ res.writeHead(401, { "Content-Type": "application/json" });
662
+ res.end(JSON.stringify({ error: "unauthorized" }));
663
+ return;
664
+ }
665
+ }
500
666
  // Health / info
501
667
  if (pathname === "/" || pathname === "/health") {
502
668
  const info = {
@@ -526,51 +692,36 @@ async function runDaemon(foreground = false) {
526
692
  }
527
693
  // Receive hire proposal
528
694
  if (pathname === "/hire" && req.method === "POST") {
529
- let body = "";
530
- req.on("data", (chunk) => { body += chunk.toString(); });
531
- req.on("end", async () => {
532
- try {
533
- const msg = JSON.parse(body);
534
- // Feed into the hire listener's message handler
535
- const proposal = {
536
- messageId: String(msg.messageId ?? msg.id ?? `http_${Date.now()}`),
537
- hirerAddress: String(msg.hirerAddress ?? msg.hirer_address ?? msg.from ?? ""),
538
- capability: String(msg.capability ?? ""),
539
- priceEth: String(msg.priceEth ?? msg.price_eth ?? "0"),
540
- deadlineUnix: Number(msg.deadlineUnix ?? msg.deadline ?? 0),
541
- specHash: String(msg.specHash ?? msg.spec_hash ?? ""),
542
- agreementId: msg.agreementId ? String(msg.agreementId) : undefined,
543
- signature: msg.signature ? String(msg.signature) : undefined,
544
- };
545
- // Dedup
546
- const existing = db.getHireRequest(proposal.messageId);
547
- if (existing) {
548
- res.writeHead(200, { "Content-Type": "application/json" });
549
- res.end(JSON.stringify({ status: existing.status, id: proposal.messageId }));
550
- return;
551
- }
552
- // Policy check
553
- const { evaluatePolicy } = await Promise.resolve().then(() => __importStar(require("./hire-listener")));
554
- const activeCount = db.countActiveHireRequests();
555
- const policyResult = evaluatePolicy(proposal, config, activeCount);
556
- if (!policyResult.allowed) {
557
- db.insertHireRequest({
558
- id: proposal.messageId,
559
- agreement_id: proposal.agreementId ?? null,
560
- hirer_address: proposal.hirerAddress,
561
- capability: proposal.capability,
562
- price_eth: proposal.priceEth,
563
- deadline_unix: proposal.deadlineUnix,
564
- spec_hash: proposal.specHash,
565
- status: "rejected",
566
- reject_reason: policyResult.reason ?? "policy_violation",
567
- });
568
- log({ event: "http_hire_rejected", id: proposal.messageId, reason: policyResult.reason });
569
- res.writeHead(200, { "Content-Type": "application/json" });
570
- res.end(JSON.stringify({ status: "rejected", reason: policyResult.reason, id: proposal.messageId }));
571
- return;
572
- }
573
- const status = config.policy.auto_accept ? "accepted" : "pending_approval";
695
+ const body = await readBody(req, res);
696
+ if (body === null)
697
+ return;
698
+ verifyRequestSignature(body, req);
699
+ try {
700
+ const msg = JSON.parse(body);
701
+ // Feed into the hire listener's message handler
702
+ const proposal = {
703
+ messageId: String(msg.messageId ?? msg.id ?? `http_${Date.now()}`),
704
+ hirerAddress: String(msg.hirerAddress ?? msg.hirer_address ?? msg.from ?? ""),
705
+ capability: String(msg.capability ?? ""),
706
+ priceEth: String(msg.priceEth ?? msg.price_eth ?? "0"),
707
+ deadlineUnix: Number(msg.deadlineUnix ?? msg.deadline ?? 0),
708
+ specHash: String(msg.specHash ?? msg.spec_hash ?? ""),
709
+ agreementId: msg.agreementId ? String(msg.agreementId) : undefined,
710
+ signature: msg.signature ? String(msg.signature) : undefined,
711
+ };
712
+ // Dedup
713
+ const existing = db.getHireRequest(proposal.messageId);
714
+ if (existing) {
715
+ log({ event: "hire_duplicate", id: proposal.messageId, status: existing.status });
716
+ res.writeHead(200, { "Content-Type": "application/json" });
717
+ res.end(JSON.stringify({ status: existing.status, id: proposal.messageId }));
718
+ return;
719
+ }
720
+ // Policy check
721
+ const { evaluatePolicy } = await Promise.resolve().then(() => __importStar(require("./hire-listener")));
722
+ const activeCount = db.countActiveHireRequests();
723
+ const policyResult = evaluatePolicy(proposal, config, activeCount);
724
+ if (!policyResult.allowed) {
574
725
  db.insertHireRequest({
575
726
  id: proposal.messageId,
576
727
  agreement_id: proposal.agreementId ?? null,
@@ -579,206 +730,227 @@ async function runDaemon(foreground = false) {
579
730
  price_eth: proposal.priceEth,
580
731
  deadline_unix: proposal.deadlineUnix,
581
732
  spec_hash: proposal.specHash,
582
- status,
583
- reject_reason: null,
733
+ status: "rejected",
734
+ reject_reason: policyResult.reason ?? "policy_violation",
584
735
  });
585
- log({ event: "http_hire_received", id: proposal.messageId, hirer: proposal.hirerAddress, status });
586
- if (config.notifications.notify_on_hire_request) {
587
- await notifier.notifyHireRequest(proposal.messageId, proposal.hirerAddress, proposal.priceEth, proposal.capability);
588
- }
736
+ log({ event: "http_hire_rejected", id: proposal.messageId, reason: policyResult.reason });
589
737
  res.writeHead(200, { "Content-Type": "application/json" });
590
- res.end(JSON.stringify({ status, id: proposal.messageId }));
738
+ res.end(JSON.stringify({ status: "rejected", reason: policyResult.reason, id: proposal.messageId }));
739
+ return;
591
740
  }
592
- catch (err) {
593
- log({ event: "http_hire_error", error: String(err) });
594
- res.writeHead(400, { "Content-Type": "application/json" });
595
- res.end(JSON.stringify({ error: "invalid_request" }));
741
+ const status = config.policy.auto_accept ? "accepted" : "pending_approval";
742
+ db.insertHireRequest({
743
+ id: proposal.messageId,
744
+ agreement_id: proposal.agreementId ?? null,
745
+ hirer_address: proposal.hirerAddress,
746
+ capability: proposal.capability,
747
+ price_eth: proposal.priceEth,
748
+ deadline_unix: proposal.deadlineUnix,
749
+ spec_hash: proposal.specHash,
750
+ status,
751
+ reject_reason: null,
752
+ });
753
+ log({ event: "http_hire_received", id: proposal.messageId, hirer: proposal.hirerAddress, status });
754
+ if (config.notifications.notify_on_hire_request) {
755
+ await notifier.notifyHireRequest(proposal.messageId, proposal.hirerAddress, proposal.priceEth, proposal.capability);
596
756
  }
597
- });
757
+ res.writeHead(200, { "Content-Type": "application/json" });
758
+ res.end(JSON.stringify({ status, id: proposal.messageId }));
759
+ }
760
+ catch (err) {
761
+ log({ event: "http_hire_error", error: String(err) });
762
+ res.writeHead(400, { "Content-Type": "application/json" });
763
+ res.end(JSON.stringify({ error: "invalid_request" }));
764
+ }
598
765
  return;
599
766
  }
600
767
  // Handshake acknowledgment endpoint
601
768
  if (pathname === "/handshake" && req.method === "POST") {
602
- let body = "";
603
- req.on("data", (chunk) => { body += chunk.toString(); });
604
- req.on("end", () => {
605
- try {
606
- const msg = JSON.parse(body);
607
- log({ event: "handshake_received", from: msg.from, type: msg.type, note: msg.note });
608
- res.writeHead(200, { "Content-Type": "application/json" });
609
- res.end(JSON.stringify({ received: true, agent: config.wallet.contract_address }));
610
- }
611
- catch {
612
- res.writeHead(400, { "Content-Type": "application/json" });
613
- res.end(JSON.stringify({ error: "invalid_request" }));
614
- }
615
- });
769
+ const body = await readBody(req, res);
770
+ if (body === null)
771
+ return;
772
+ verifyRequestSignature(body, req);
773
+ try {
774
+ const msg = JSON.parse(body);
775
+ log({ event: "handshake_received", from: msg.from, type: msg.type, note: msg.note });
776
+ res.writeHead(200, { "Content-Type": "application/json" });
777
+ res.end(JSON.stringify({ received: true, agent: config.wallet.contract_address }));
778
+ }
779
+ catch {
780
+ res.writeHead(400, { "Content-Type": "application/json" });
781
+ res.end(JSON.stringify({ error: "invalid_request" }));
782
+ }
616
783
  return;
617
784
  }
618
785
  // POST /hire/accepted — provider accepted, client notified
619
786
  if (pathname === "/hire/accepted" && req.method === "POST") {
620
- let body = "";
621
- req.on("data", (chunk) => { body += chunk.toString(); });
622
- req.on("end", async () => {
623
- try {
624
- const msg = JSON.parse(body);
625
- const agreementId = String(msg.agreementId ?? msg.agreement_id ?? "");
626
- const from = String(msg.from ?? "");
627
- log({ event: "hire_accepted_inbound", agreementId, from });
628
- if (config.notifications.notify_on_hire_accepted) {
629
- await notifier.notifyHireAccepted(agreementId, agreementId);
630
- }
631
- res.writeHead(200, { "Content-Type": "application/json" });
632
- res.end(JSON.stringify({ received: true, agent: config.wallet.contract_address }));
787
+ const body = await readBody(req, res);
788
+ if (body === null)
789
+ return;
790
+ try {
791
+ const msg = JSON.parse(body);
792
+ const agreementId = String(msg.agreementId ?? msg.agreement_id ?? "");
793
+ const from = String(msg.from ?? "");
794
+ log({ event: "hire_accepted_inbound", agreementId, from });
795
+ if (config.notifications.notify_on_hire_accepted) {
796
+ await notifier.notifyHireAccepted(agreementId, agreementId);
633
797
  }
634
- catch {
635
- res.writeHead(400, { "Content-Type": "application/json" });
636
- res.end(JSON.stringify({ error: "invalid_request" }));
637
- }
638
- });
798
+ res.writeHead(200, { "Content-Type": "application/json" });
799
+ res.end(JSON.stringify({ received: true, agent: config.wallet.contract_address }));
800
+ }
801
+ catch {
802
+ res.writeHead(400, { "Content-Type": "application/json" });
803
+ res.end(JSON.stringify({ error: "invalid_request" }));
804
+ }
639
805
  return;
640
806
  }
641
807
  // POST /message — off-chain negotiation message
642
808
  if (pathname === "/message" && req.method === "POST") {
643
- let body = "";
644
- req.on("data", (chunk) => { body += chunk.toString(); });
645
- req.on("end", () => {
646
- try {
647
- const msg = JSON.parse(body);
648
- const from = String(msg.from ?? "");
649
- const to = String(msg.to ?? "");
650
- const content = String(msg.content ?? "");
651
- const timestamp = Number(msg.timestamp ?? Date.now());
652
- log({ event: "message_received", from, to, timestamp, content_len: content.length });
653
- res.writeHead(200, { "Content-Type": "application/json" });
654
- res.end(JSON.stringify({ received: true, agent: config.wallet.contract_address }));
655
- }
656
- catch {
657
- res.writeHead(400, { "Content-Type": "application/json" });
658
- res.end(JSON.stringify({ error: "invalid_request" }));
659
- }
660
- });
809
+ const body = await readBody(req, res);
810
+ if (body === null)
811
+ return;
812
+ verifyRequestSignature(body, req);
813
+ try {
814
+ const msg = JSON.parse(body);
815
+ const from = String(msg.from ?? "");
816
+ const to = String(msg.to ?? "");
817
+ const content = String(msg.content ?? "");
818
+ const timestamp = Number(msg.timestamp ?? Date.now());
819
+ log({ event: "message_received", from, to, timestamp, content_len: content.length });
820
+ res.writeHead(200, { "Content-Type": "application/json" });
821
+ res.end(JSON.stringify({ received: true, agent: config.wallet.contract_address }));
822
+ }
823
+ catch {
824
+ res.writeHead(400, { "Content-Type": "application/json" });
825
+ res.end(JSON.stringify({ error: "invalid_request" }));
826
+ }
661
827
  return;
662
828
  }
663
829
  // POST /delivery — provider committed a deliverable
664
830
  if (pathname === "/delivery" && req.method === "POST") {
665
- let body = "";
666
- req.on("data", (chunk) => { body += chunk.toString(); });
667
- req.on("end", async () => {
668
- try {
669
- const msg = JSON.parse(body);
670
- const agreementId = String(msg.agreementId ?? msg.agreement_id ?? "");
671
- const deliverableHash = String(msg.deliverableHash ?? msg.deliverable_hash ?? "");
672
- const from = String(msg.from ?? "");
673
- log({ event: "delivery_received", agreementId, deliverableHash, from });
674
- // Update DB: mark delivered
675
- const active = db.listActiveHireRequests();
676
- const found = active.find(r => r.agreement_id === agreementId);
677
- if (found)
678
- db.updateHireRequestStatus(found.id, "delivered");
679
- if (config.notifications.notify_on_delivery) {
680
- await notifier.notifyDelivery(agreementId, deliverableHash, "");
681
- }
682
- res.writeHead(200, { "Content-Type": "application/json" });
683
- res.end(JSON.stringify({ received: true, agent: config.wallet.contract_address }));
831
+ const body = await readBody(req, res);
832
+ if (body === null)
833
+ return;
834
+ verifyRequestSignature(body, req);
835
+ try {
836
+ const msg = JSON.parse(body);
837
+ const agreementId = String(msg.agreementId ?? msg.agreement_id ?? "");
838
+ const deliverableHash = String(msg.deliverableHash ?? msg.deliverable_hash ?? "");
839
+ const filesUrl = String(msg.files_url ?? msg.filesUrl ?? "");
840
+ const from = String(msg.from ?? "");
841
+ log({ event: "delivery_received", agreementId, deliverableHash, from, has_files_url: !!filesUrl });
842
+ // Update DB: mark delivered
843
+ const active = db.listActiveHireRequests();
844
+ const found = active.find(r => r.agreement_id === agreementId);
845
+ if (found)
846
+ db.updateHireRequestStatus(found.id, "delivered");
847
+ if (config.notifications.notify_on_delivery) {
848
+ await notifier.notifyDelivery(agreementId, deliverableHash, "");
684
849
  }
685
- catch {
686
- res.writeHead(400, { "Content-Type": "application/json" });
687
- res.end(JSON.stringify({ error: "invalid_request" }));
850
+ // Auto-download and verify if files_url provided and auto_download enabled
851
+ if (filesUrl && config.delivery.auto_download) {
852
+ void deliveryClient.handleDeliveryNotification({ agreementId, deliverableHash, filesUrl })
853
+ .then(result => {
854
+ if (result)
855
+ log({ event: "delivery_auto_downloaded", agreementId, ok: result.ok, root_hash_match: result.rootHashMatch, files: result.fileResults.length });
856
+ })
857
+ .catch(err => log({ event: "delivery_download_error", agreementId, error: String(err) }));
688
858
  }
689
- });
859
+ res.writeHead(200, { "Content-Type": "application/json" });
860
+ res.end(JSON.stringify({ received: true, agent: config.wallet.contract_address }));
861
+ }
862
+ catch {
863
+ res.writeHead(400, { "Content-Type": "application/json" });
864
+ res.end(JSON.stringify({ error: "invalid_request" }));
865
+ }
690
866
  return;
691
867
  }
692
868
  // POST /delivery/accepted — client accepted delivery, payment releasing
693
869
  if (pathname === "/delivery/accepted" && req.method === "POST") {
694
- let body = "";
695
- req.on("data", (chunk) => { body += chunk.toString(); });
696
- req.on("end", () => {
697
- try {
698
- const msg = JSON.parse(body);
699
- const agreementId = String(msg.agreementId ?? msg.agreement_id ?? "");
700
- const from = String(msg.from ?? "");
701
- log({ event: "delivery_accepted_inbound", agreementId, from });
702
- // Update DB: mark complete
703
- const all = db.listActiveHireRequests();
704
- const found = all.find(r => r.agreement_id === agreementId);
705
- if (found)
706
- db.updateHireRequestStatus(found.id, "complete");
707
- res.writeHead(200, { "Content-Type": "application/json" });
708
- res.end(JSON.stringify({ received: true, agent: config.wallet.contract_address }));
709
- }
710
- catch {
711
- res.writeHead(400, { "Content-Type": "application/json" });
712
- res.end(JSON.stringify({ error: "invalid_request" }));
713
- }
714
- });
870
+ const body = await readBody(req, res);
871
+ if (body === null)
872
+ return;
873
+ try {
874
+ const msg = JSON.parse(body);
875
+ const agreementId = String(msg.agreementId ?? msg.agreement_id ?? "");
876
+ const from = String(msg.from ?? "");
877
+ log({ event: "delivery_accepted_inbound", agreementId, from });
878
+ // Update DB: mark complete
879
+ const all = db.listActiveHireRequests();
880
+ const found = all.find(r => r.agreement_id === agreementId);
881
+ if (found)
882
+ db.updateHireRequestStatus(found.id, "complete");
883
+ res.writeHead(200, { "Content-Type": "application/json" });
884
+ res.end(JSON.stringify({ received: true, agent: config.wallet.contract_address }));
885
+ }
886
+ catch {
887
+ res.writeHead(400, { "Content-Type": "application/json" });
888
+ res.end(JSON.stringify({ error: "invalid_request" }));
889
+ }
715
890
  return;
716
891
  }
717
892
  // POST /dispute — dispute raised against this agent
718
893
  if (pathname === "/dispute" && req.method === "POST") {
719
- let body = "";
720
- req.on("data", (chunk) => { body += chunk.toString(); });
721
- req.on("end", async () => {
722
- try {
723
- const msg = JSON.parse(body);
724
- const agreementId = String(msg.agreementId ?? msg.agreement_id ?? "");
725
- const reason = String(msg.reason ?? "");
726
- const from = String(msg.from ?? "");
727
- log({ event: "dispute_received", agreementId, reason, from });
728
- if (config.notifications.notify_on_dispute) {
729
- await notifier.notifyDispute(agreementId, from);
730
- }
731
- res.writeHead(200, { "Content-Type": "application/json" });
732
- res.end(JSON.stringify({ received: true, agent: config.wallet.contract_address }));
894
+ const body = await readBody(req, res);
895
+ if (body === null)
896
+ return;
897
+ try {
898
+ const msg = JSON.parse(body);
899
+ const agreementId = String(msg.agreementId ?? msg.agreement_id ?? "");
900
+ const reason = String(msg.reason ?? "");
901
+ const from = String(msg.from ?? "");
902
+ log({ event: "dispute_received", agreementId, reason, from });
903
+ if (config.notifications.notify_on_dispute) {
904
+ await notifier.notifyDispute(agreementId, from);
733
905
  }
734
- catch {
735
- res.writeHead(400, { "Content-Type": "application/json" });
736
- res.end(JSON.stringify({ error: "invalid_request" }));
737
- }
738
- });
906
+ res.writeHead(200, { "Content-Type": "application/json" });
907
+ res.end(JSON.stringify({ received: true, agent: config.wallet.contract_address }));
908
+ }
909
+ catch {
910
+ res.writeHead(400, { "Content-Type": "application/json" });
911
+ res.end(JSON.stringify({ error: "invalid_request" }));
912
+ }
739
913
  return;
740
914
  }
741
915
  // POST /dispute/resolved — dispute resolved by arbitrator
742
916
  if (pathname === "/dispute/resolved" && req.method === "POST") {
743
- let body = "";
744
- req.on("data", (chunk) => { body += chunk.toString(); });
745
- req.on("end", () => {
746
- try {
747
- const msg = JSON.parse(body);
748
- const agreementId = String(msg.agreementId ?? msg.agreement_id ?? "");
749
- const outcome = String(msg.outcome ?? "");
750
- const from = String(msg.from ?? "");
751
- log({ event: "dispute_resolved_inbound", agreementId, outcome, from });
752
- res.writeHead(200, { "Content-Type": "application/json" });
753
- res.end(JSON.stringify({ received: true, agent: config.wallet.contract_address }));
754
- }
755
- catch {
756
- res.writeHead(400, { "Content-Type": "application/json" });
757
- res.end(JSON.stringify({ error: "invalid_request" }));
758
- }
759
- });
917
+ const body = await readBody(req, res);
918
+ if (body === null)
919
+ return;
920
+ try {
921
+ const msg = JSON.parse(body);
922
+ const agreementId = String(msg.agreementId ?? msg.agreement_id ?? "");
923
+ const outcome = String(msg.outcome ?? "");
924
+ const from = String(msg.from ?? "");
925
+ log({ event: "dispute_resolved_inbound", agreementId, outcome, from });
926
+ res.writeHead(200, { "Content-Type": "application/json" });
927
+ res.end(JSON.stringify({ received: true, agent: config.wallet.contract_address }));
928
+ }
929
+ catch {
930
+ res.writeHead(400, { "Content-Type": "application/json" });
931
+ res.end(JSON.stringify({ error: "invalid_request" }));
932
+ }
760
933
  return;
761
934
  }
762
935
  // POST /workroom/status — workroom lifecycle events
763
936
  if (pathname === "/workroom/status" && req.method === "POST") {
764
- let body = "";
765
- req.on("data", (chunk) => { body += chunk.toString(); });
766
- req.on("end", () => {
767
- try {
768
- const msg = JSON.parse(body);
769
- const event = String(msg.event ?? "");
770
- const agentAddress = String(msg.agentAddress ?? config.wallet.contract_address);
771
- const jobId = msg.jobId ? String(msg.jobId) : undefined;
772
- const timestamp = Number(msg.timestamp ?? Date.now());
773
- log({ event: "workroom_lifecycle", workroom_event: event, agentAddress, jobId, timestamp });
774
- res.writeHead(200, { "Content-Type": "application/json" });
775
- res.end(JSON.stringify({ received: true, agent: config.wallet.contract_address }));
776
- }
777
- catch {
778
- res.writeHead(400, { "Content-Type": "application/json" });
779
- res.end(JSON.stringify({ error: "invalid_request" }));
780
- }
781
- });
937
+ const body = await readBody(req, res);
938
+ if (body === null)
939
+ return;
940
+ try {
941
+ const msg = JSON.parse(body);
942
+ const event = String(msg.event ?? "");
943
+ const agentAddress = String(msg.agentAddress ?? config.wallet.contract_address);
944
+ const jobId = msg.jobId ? String(msg.jobId) : undefined;
945
+ const timestamp = Number(msg.timestamp ?? Date.now());
946
+ log({ event: "workroom_lifecycle", workroom_event: event, agentAddress, jobId, timestamp });
947
+ res.writeHead(200, { "Content-Type": "application/json" });
948
+ res.end(JSON.stringify({ received: true, agent: config.wallet.contract_address }));
949
+ }
950
+ catch {
951
+ res.writeHead(400, { "Content-Type": "application/json" });
952
+ res.end(JSON.stringify({ error: "invalid_request" }));
953
+ }
782
954
  return;
783
955
  }
784
956
  // GET /capabilities — agent capabilities from config
@@ -792,23 +964,415 @@ async function runDaemon(foreground = false) {
792
964
  }));
793
965
  return;
794
966
  }
795
- // GET /status — health with active agreement count
967
+ // GET /status — health with active agreement count (sensitive counts only for authenticated)
796
968
  if (pathname === "/status" && req.method === "GET") {
797
- const activeList = db.listActiveHireRequests();
798
- const pendingList = db.listPendingHireRequests();
799
- res.writeHead(200, { "Content-Type": "application/json" });
800
- res.end(JSON.stringify({
969
+ const statusAuth = (req.headers["authorization"] ?? "");
970
+ const statusToken = statusAuth.startsWith("Bearer ") ? statusAuth.slice(7) : "";
971
+ const statusAuthed = statusToken === apiToken;
972
+ const statusPayload = {
801
973
  protocol: "arc-402",
802
974
  version: "0.3.0",
803
975
  agent: config.wallet.contract_address,
804
976
  status: "online",
805
977
  uptime: Math.floor((Date.now() - ipcCtx.startTime) / 1000),
806
- active_agreements: activeList.length,
807
- pending_approval: pendingList.length,
808
978
  capabilities: config.policy.allowed_capabilities,
809
- }));
979
+ };
980
+ if (statusAuthed) {
981
+ statusPayload.active_agreements = db.listActiveHireRequests().length;
982
+ statusPayload.pending_approval = db.listPendingHireRequests().length;
983
+ if (config.delivery.serve_files) {
984
+ const deliveriesDir = path.join(config_1.DAEMON_DIR, "deliveries");
985
+ let totalDeliveries = 0, totalFiles = 0;
986
+ if (fs.existsSync(deliveriesDir)) {
987
+ const entries = fs.readdirSync(deliveriesDir);
988
+ totalDeliveries = entries.length;
989
+ for (const entry of entries) {
990
+ const entryPath = path.join(deliveriesDir, entry);
991
+ if (fs.statSync(entryPath).isDirectory()) {
992
+ totalFiles += fs.readdirSync(entryPath).filter(f => f !== "_manifest.json").length;
993
+ }
994
+ }
995
+ }
996
+ statusPayload.file_delivery = { total_deliveries: totalDeliveries, total_files: totalFiles };
997
+ }
998
+ }
999
+ res.writeHead(200, { "Content-Type": "application/json" });
1000
+ res.end(JSON.stringify(statusPayload));
1001
+ return;
1002
+ }
1003
+ // ── Compute rental routes ─────────────────────────────────────────────
1004
+ // POST /compute/propose — client proposes a compute session
1005
+ if (pathname === "/compute/propose" && req.method === "POST") {
1006
+ const body = await readBody(req, res);
1007
+ if (body === null)
1008
+ return;
1009
+ try {
1010
+ const msg = JSON.parse(body);
1011
+ if (!computeSessions) {
1012
+ res.writeHead(503, { "Content-Type": "application/json" });
1013
+ res.end(JSON.stringify({ error: "compute_disabled" }));
1014
+ return;
1015
+ }
1016
+ const proposal = {
1017
+ sessionId: String(msg.sessionId ?? ""),
1018
+ clientAddress: String(msg.clientAddress ?? msg.client ?? ""),
1019
+ providerAddress: config.wallet.contract_address,
1020
+ ratePerHourWei: config.compute.rate_per_hour_wei,
1021
+ maxHours: Number(msg.maxHours ?? 1),
1022
+ gpuSpecHash: String(msg.gpuSpecHash ?? "0x0000000000000000000000000000000000000000000000000000000000000000"),
1023
+ workloadDescription: String(msg.workloadDescription ?? ""),
1024
+ depositAmount: String(msg.depositAmount ?? "0"),
1025
+ proposedAt: Math.floor(Date.now() / 1000),
1026
+ };
1027
+ if (!proposal.sessionId) {
1028
+ res.writeHead(400, { "Content-Type": "application/json" });
1029
+ res.end(JSON.stringify({ error: "sessionId required" }));
1030
+ return;
1031
+ }
1032
+ // Check capacity
1033
+ const activeSessions = computeSessions.countByStatus("active");
1034
+ if (activeSessions >= config.compute.max_concurrent_sessions) {
1035
+ res.writeHead(200, { "Content-Type": "application/json" });
1036
+ res.end(JSON.stringify({ status: "rejected", reason: "at_capacity" }));
1037
+ return;
1038
+ }
1039
+ const result = computeSessions.handleProposal(proposal);
1040
+ if (!result.ok) {
1041
+ res.writeHead(400, { "Content-Type": "application/json" });
1042
+ res.end(JSON.stringify({ error: result.error }));
1043
+ return;
1044
+ }
1045
+ log({ event: "compute_proposed", sessionId: proposal.sessionId, client: proposal.clientAddress });
1046
+ // Auto-accept if configured
1047
+ if (config.compute.auto_accept_compute) {
1048
+ computeSessions.acceptSession(proposal.sessionId);
1049
+ log({ event: "compute_auto_accepted", sessionId: proposal.sessionId });
1050
+ // Wire to on-chain: provider calls acceptSession
1051
+ if (config.compute.compute_agreement_address) {
1052
+ try {
1053
+ const caContract = new ethers_1.ethers.Contract(config.compute.compute_agreement_address, abis_1.COMPUTE_AGREEMENT_ABI, machineKeySigner);
1054
+ const tx = await caContract.acceptSession(proposal.sessionId);
1055
+ await tx.wait();
1056
+ log({ event: "compute_accept_onchain", sessionId: proposal.sessionId, txHash: tx.hash });
1057
+ }
1058
+ catch (onchainErr) {
1059
+ log({ event: "compute_accept_onchain_error", sessionId: proposal.sessionId, error: String(onchainErr) });
1060
+ }
1061
+ }
1062
+ }
1063
+ const status = config.compute.auto_accept_compute ? "accepted" : "proposed";
1064
+ res.writeHead(200, { "Content-Type": "application/json" });
1065
+ res.end(JSON.stringify({ status, sessionId: proposal.sessionId }));
1066
+ }
1067
+ catch (err) {
1068
+ log({ event: "compute_propose_error", error: String(err) });
1069
+ res.writeHead(400, { "Content-Type": "application/json" });
1070
+ res.end(JSON.stringify({ error: "invalid_request" }));
1071
+ }
1072
+ return;
1073
+ }
1074
+ // POST /compute/accept — provider accepts a proposed session
1075
+ if (pathname === "/compute/accept" && req.method === "POST") {
1076
+ const body = await readBody(req, res);
1077
+ if (body === null)
1078
+ return;
1079
+ try {
1080
+ const msg = JSON.parse(body);
1081
+ const sessionId = String(msg.sessionId ?? "");
1082
+ if (!computeSessions) {
1083
+ res.writeHead(503, { "Content-Type": "application/json" });
1084
+ res.end(JSON.stringify({ error: "compute_disabled" }));
1085
+ return;
1086
+ }
1087
+ const result = computeSessions.acceptSession(sessionId);
1088
+ if (!result.ok) {
1089
+ res.writeHead(400, { "Content-Type": "application/json" });
1090
+ res.end(JSON.stringify({ error: result.error }));
1091
+ return;
1092
+ }
1093
+ log({ event: "compute_accepted", sessionId });
1094
+ // Wire to on-chain: provider calls acceptSession
1095
+ if (config.compute.compute_agreement_address) {
1096
+ try {
1097
+ const caContract = new ethers_1.ethers.Contract(config.compute.compute_agreement_address, abis_1.COMPUTE_AGREEMENT_ABI, machineKeySigner);
1098
+ const tx = await caContract.acceptSession(sessionId);
1099
+ await tx.wait();
1100
+ log({ event: "compute_accept_onchain", sessionId, txHash: tx.hash });
1101
+ }
1102
+ catch (onchainErr) {
1103
+ log({ event: "compute_accept_onchain_error", sessionId, error: String(onchainErr) });
1104
+ }
1105
+ }
1106
+ res.writeHead(200, { "Content-Type": "application/json" });
1107
+ res.end(JSON.stringify({ status: "accepted", sessionId }));
1108
+ }
1109
+ catch (err) {
1110
+ log({ event: "compute_accept_error", error: String(err) });
1111
+ res.writeHead(400, { "Content-Type": "application/json" });
1112
+ res.end(JSON.stringify({ error: "invalid_request" }));
1113
+ }
1114
+ return;
1115
+ }
1116
+ // POST /compute/started — provider marks session as started
1117
+ if (pathname === "/compute/started" && req.method === "POST") {
1118
+ const body = await readBody(req, res);
1119
+ if (body === null)
1120
+ return;
1121
+ try {
1122
+ const msg = JSON.parse(body);
1123
+ const sessionId = String(msg.sessionId ?? "");
1124
+ const accessEndpoint = msg.accessEndpoint ? String(msg.accessEndpoint) : undefined;
1125
+ if (!computeSessions) {
1126
+ res.writeHead(503, { "Content-Type": "application/json" });
1127
+ res.end(JSON.stringify({ error: "compute_disabled" }));
1128
+ return;
1129
+ }
1130
+ const result = computeSessions.startSession(sessionId, accessEndpoint);
1131
+ if (!result.ok) {
1132
+ res.writeHead(400, { "Content-Type": "application/json" });
1133
+ res.end(JSON.stringify({ error: result.error }));
1134
+ return;
1135
+ }
1136
+ log({ event: "compute_started", sessionId });
1137
+ // Wire to on-chain: provider calls startSession
1138
+ if (config.compute.compute_agreement_address) {
1139
+ try {
1140
+ const caContract = new ethers_1.ethers.Contract(config.compute.compute_agreement_address, abis_1.COMPUTE_AGREEMENT_ABI, machineKeySigner);
1141
+ const tx = await caContract.startSession(sessionId);
1142
+ await tx.wait();
1143
+ log({ event: "compute_start_onchain", sessionId, txHash: tx.hash });
1144
+ }
1145
+ catch (onchainErr) {
1146
+ log({ event: "compute_start_onchain_error", sessionId, error: String(onchainErr) });
1147
+ }
1148
+ }
1149
+ res.writeHead(200, { "Content-Type": "application/json" });
1150
+ res.end(JSON.stringify({ status: "active", sessionId }));
1151
+ }
1152
+ catch (err) {
1153
+ log({ event: "compute_started_error", error: String(err) });
1154
+ res.writeHead(400, { "Content-Type": "application/json" });
1155
+ res.end(JSON.stringify({ error: "invalid_request" }));
1156
+ }
1157
+ return;
1158
+ }
1159
+ // POST /compute/metrics — get current metrics (polling endpoint)
1160
+ if (pathname === "/compute/metrics" && req.method === "POST") {
1161
+ const body = await readBody(req, res);
1162
+ if (body === null)
1163
+ return;
1164
+ try {
1165
+ const msg = JSON.parse(body);
1166
+ const sessionId = String(msg.sessionId ?? "");
1167
+ if (!computeMetering || !computeSessions) {
1168
+ res.writeHead(503, { "Content-Type": "application/json" });
1169
+ res.end(JSON.stringify({ error: "compute_disabled" }));
1170
+ return;
1171
+ }
1172
+ const session = computeSessions.getSession(sessionId);
1173
+ if (!session) {
1174
+ res.writeHead(404, { "Content-Type": "application/json" });
1175
+ res.end(JSON.stringify({ error: "session_not_found" }));
1176
+ return;
1177
+ }
1178
+ const current = computeMetering.getCurrentMetrics(sessionId);
1179
+ const reports = computeMetering.getUsageReports(sessionId);
1180
+ res.writeHead(200, { "Content-Type": "application/json" });
1181
+ res.end(JSON.stringify({ sessionId, current, reports, consumedMinutes: session.consumedMinutes }));
1182
+ }
1183
+ catch (err) {
1184
+ log({ event: "compute_metrics_error", error: String(err) });
1185
+ res.writeHead(400, { "Content-Type": "application/json" });
1186
+ res.end(JSON.stringify({ error: "invalid_request" }));
1187
+ }
1188
+ return;
1189
+ }
1190
+ // POST /compute/end — either party ends the session
1191
+ if (pathname === "/compute/end" && req.method === "POST") {
1192
+ const body = await readBody(req, res);
1193
+ if (body === null)
1194
+ return;
1195
+ try {
1196
+ const msg = JSON.parse(body);
1197
+ const sessionId = String(msg.sessionId ?? "");
1198
+ if (!computeSessions) {
1199
+ res.writeHead(503, { "Content-Type": "application/json" });
1200
+ res.end(JSON.stringify({ error: "compute_disabled" }));
1201
+ return;
1202
+ }
1203
+ const endResult = await computeSessions.endSession(sessionId);
1204
+ if (!endResult.ok) {
1205
+ res.writeHead(400, { "Content-Type": "application/json" });
1206
+ res.end(JSON.stringify({ error: endResult.error }));
1207
+ return;
1208
+ }
1209
+ const r = endResult.result;
1210
+ log({
1211
+ event: "compute_ended",
1212
+ sessionId,
1213
+ consumedMinutes: r.consumedMinutes,
1214
+ costWei: r.costWei.toString(),
1215
+ refundWei: r.refundWei.toString(),
1216
+ });
1217
+ // Wire to on-chain: call endSession
1218
+ if (config.compute.compute_agreement_address) {
1219
+ try {
1220
+ const caContract = new ethers_1.ethers.Contract(config.compute.compute_agreement_address, abis_1.COMPUTE_AGREEMENT_ABI, machineKeySigner);
1221
+ const tx = await caContract.endSession(sessionId);
1222
+ await tx.wait();
1223
+ log({ event: "compute_end_onchain", sessionId, txHash: tx.hash });
1224
+ }
1225
+ catch (onchainErr) {
1226
+ log({ event: "compute_end_onchain_error", sessionId, error: String(onchainErr) });
1227
+ }
1228
+ }
1229
+ res.writeHead(200, { "Content-Type": "application/json" });
1230
+ res.end(JSON.stringify({
1231
+ status: "completed",
1232
+ sessionId,
1233
+ consumedMinutes: r.consumedMinutes,
1234
+ costWei: r.costWei.toString(),
1235
+ refundWei: r.refundWei.toString(),
1236
+ reports: r.reports,
1237
+ }));
1238
+ }
1239
+ catch (err) {
1240
+ log({ event: "compute_end_error", error: String(err) });
1241
+ res.writeHead(400, { "Content-Type": "application/json" });
1242
+ res.end(JSON.stringify({ error: "invalid_request" }));
1243
+ }
1244
+ return;
1245
+ }
1246
+ // POST /compute/dispute — client disputes a session
1247
+ if (pathname === "/compute/dispute" && req.method === "POST") {
1248
+ const body = await readBody(req, res);
1249
+ if (body === null)
1250
+ return;
1251
+ try {
1252
+ const msg = JSON.parse(body);
1253
+ const sessionId = String(msg.sessionId ?? "");
1254
+ if (!computeSessions) {
1255
+ res.writeHead(503, { "Content-Type": "application/json" });
1256
+ res.end(JSON.stringify({ error: "compute_disabled" }));
1257
+ return;
1258
+ }
1259
+ const result = computeSessions.disputeSession(sessionId);
1260
+ if (!result.ok) {
1261
+ res.writeHead(400, { "Content-Type": "application/json" });
1262
+ res.end(JSON.stringify({ error: result.error }));
1263
+ return;
1264
+ }
1265
+ log({ event: "compute_disputed", sessionId });
1266
+ if (config.notifications.notify_on_dispute) {
1267
+ await notifier.notifyDispute(sessionId, String(msg.from ?? "client"));
1268
+ }
1269
+ // Wire to on-chain: call disputeSession
1270
+ if (config.compute.compute_agreement_address) {
1271
+ try {
1272
+ const caContract = new ethers_1.ethers.Contract(config.compute.compute_agreement_address, abis_1.COMPUTE_AGREEMENT_ABI, machineKeySigner);
1273
+ const tx = await caContract.disputeSession(sessionId);
1274
+ await tx.wait();
1275
+ log({ event: "compute_dispute_onchain", sessionId, txHash: tx.hash });
1276
+ }
1277
+ catch (onchainErr) {
1278
+ log({ event: "compute_dispute_onchain_error", sessionId, error: String(onchainErr) });
1279
+ }
1280
+ }
1281
+ res.writeHead(200, { "Content-Type": "application/json" });
1282
+ res.end(JSON.stringify({ status: "disputed", sessionId }));
1283
+ }
1284
+ catch (err) {
1285
+ log({ event: "compute_dispute_error", error: String(err) });
1286
+ res.writeHead(400, { "Content-Type": "application/json" });
1287
+ res.end(JSON.stringify({ error: "invalid_request" }));
1288
+ }
810
1289
  return;
811
1290
  }
1291
+ // GET /compute/session/:id — session details
1292
+ if (pathname.startsWith("/compute/session/") && req.method === "GET") {
1293
+ const sessionId = pathname.slice("/compute/session/".length);
1294
+ if (!computeSessions) {
1295
+ res.writeHead(503, { "Content-Type": "application/json" });
1296
+ res.end(JSON.stringify({ error: "compute_disabled" }));
1297
+ return;
1298
+ }
1299
+ const session = computeSessions.getSession(sessionId);
1300
+ if (!session) {
1301
+ res.writeHead(404, { "Content-Type": "application/json" });
1302
+ res.end(JSON.stringify({ error: "session_not_found" }));
1303
+ return;
1304
+ }
1305
+ const current = computeMetering ? computeMetering.getCurrentMetrics(sessionId) : null;
1306
+ res.writeHead(200, { "Content-Type": "application/json" });
1307
+ res.end(JSON.stringify({ session, current }));
1308
+ return;
1309
+ }
1310
+ // GET /compute/sessions — list all sessions
1311
+ if (pathname === "/compute/sessions" && req.method === "GET") {
1312
+ if (!computeSessions) {
1313
+ res.writeHead(503, { "Content-Type": "application/json" });
1314
+ res.end(JSON.stringify({ error: "compute_disabled" }));
1315
+ return;
1316
+ }
1317
+ const sessions = computeSessions.listSessions();
1318
+ res.writeHead(200, { "Content-Type": "application/json" });
1319
+ res.end(JSON.stringify({ sessions, count: sessions.length }));
1320
+ return;
1321
+ }
1322
+ // ── File delivery routes ──────────────────────────────────────────────
1323
+ // POST /job/:id/upload — store a file (daemon auth required via global gate above)
1324
+ if (pathname.startsWith("/job/") && req.method === "POST") {
1325
+ const parts = pathname.split("/");
1326
+ // /job/<id>/upload → ["", "job", "<id>", "upload"]
1327
+ if (parts.length === 4 && parts[3] === "upload") {
1328
+ const agreementId = parts[2];
1329
+ if (!config.delivery.serve_files) {
1330
+ res.writeHead(503, { "Content-Type": "application/json" });
1331
+ res.end(JSON.stringify({ error: "file_delivery_disabled" }));
1332
+ return;
1333
+ }
1334
+ await fileDelivery.handleUpload(req, res, agreementId, log);
1335
+ return;
1336
+ }
1337
+ }
1338
+ // GET /job/:id/files, GET /job/:id/files/:name, GET /job/:id/manifest — party-gated
1339
+ if (pathname.startsWith("/job/") && req.method === "GET") {
1340
+ const parts = pathname.split("/");
1341
+ // /job/<id>/files/<name> → ["", "job", "<id>", "files", "<name>"]
1342
+ if (parts.length === 5 && parts[3] === "files") {
1343
+ const agreementId = parts[2];
1344
+ const filename = decodeURIComponent(parts[4]);
1345
+ if (!config.delivery.serve_files) {
1346
+ res.writeHead(503, { "Content-Type": "application/json" });
1347
+ res.end(JSON.stringify({ error: "file_delivery_disabled" }));
1348
+ return;
1349
+ }
1350
+ fileDelivery.handleDownloadFile(req, res, agreementId, filename, apiToken, log);
1351
+ return;
1352
+ }
1353
+ // /job/<id>/files → ["", "job", "<id>", "files"]
1354
+ if (parts.length === 4 && parts[3] === "files") {
1355
+ const agreementId = parts[2];
1356
+ if (!config.delivery.serve_files) {
1357
+ res.writeHead(503, { "Content-Type": "application/json" });
1358
+ res.end(JSON.stringify({ error: "file_delivery_disabled" }));
1359
+ return;
1360
+ }
1361
+ fileDelivery.handleListFiles(req, res, agreementId, apiToken, log);
1362
+ return;
1363
+ }
1364
+ // /job/<id>/manifest → ["", "job", "<id>", "manifest"]
1365
+ if (parts.length === 4 && parts[3] === "manifest") {
1366
+ const agreementId = parts[2];
1367
+ if (!config.delivery.serve_files) {
1368
+ res.writeHead(503, { "Content-Type": "application/json" });
1369
+ res.end(JSON.stringify({ error: "file_delivery_disabled" }));
1370
+ return;
1371
+ }
1372
+ fileDelivery.handleManifest(req, res, agreementId, apiToken, log);
1373
+ return;
1374
+ }
1375
+ }
812
1376
  // 404
813
1377
  res.writeHead(404, { "Content-Type": "application/json" });
814
1378
  res.end(JSON.stringify({ error: "not_found" }));
@@ -838,6 +1402,8 @@ async function runDaemon(foreground = false) {
838
1402
  clearInterval(relayInterval);
839
1403
  clearInterval(timeoutInterval);
840
1404
  clearInterval(balanceInterval);
1405
+ if (rateLimitCleanupInterval)
1406
+ clearInterval(rateLimitCleanupInterval);
841
1407
  // Close HTTP + IPC
842
1408
  httpServer.close();
843
1409
  ipcServer.close();