arc402-cli 0.6.0 → 0.7.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 (56) hide show
  1. package/dist/commands/backup.d.ts +3 -0
  2. package/dist/commands/backup.d.ts.map +1 -0
  3. package/dist/commands/backup.js +106 -0
  4. package/dist/commands/backup.js.map +1 -0
  5. package/dist/commands/config.d.ts.map +1 -1
  6. package/dist/commands/config.js +11 -1
  7. package/dist/commands/config.js.map +1 -1
  8. package/dist/commands/discover.d.ts.map +1 -1
  9. package/dist/commands/discover.js +60 -15
  10. package/dist/commands/discover.js.map +1 -1
  11. package/dist/commands/doctor.d.ts +3 -0
  12. package/dist/commands/doctor.d.ts.map +1 -0
  13. package/dist/commands/doctor.js +205 -0
  14. package/dist/commands/doctor.js.map +1 -0
  15. package/dist/commands/wallet.d.ts.map +1 -1
  16. package/dist/commands/wallet.js +192 -58
  17. package/dist/commands/wallet.js.map +1 -1
  18. package/dist/commands/watch.d.ts.map +1 -1
  19. package/dist/commands/watch.js +146 -9
  20. package/dist/commands/watch.js.map +1 -1
  21. package/dist/config.d.ts +9 -0
  22. package/dist/config.d.ts.map +1 -1
  23. package/dist/config.js +35 -3
  24. package/dist/config.js.map +1 -1
  25. package/dist/daemon/index.d.ts.map +1 -1
  26. package/dist/daemon/index.js +359 -220
  27. package/dist/daemon/index.js.map +1 -1
  28. package/dist/endpoint-notify.d.ts +9 -1
  29. package/dist/endpoint-notify.d.ts.map +1 -1
  30. package/dist/endpoint-notify.js +116 -3
  31. package/dist/endpoint-notify.js.map +1 -1
  32. package/dist/index.js +26 -0
  33. package/dist/index.js.map +1 -1
  34. package/dist/program.d.ts.map +1 -1
  35. package/dist/program.js +4 -0
  36. package/dist/program.js.map +1 -1
  37. package/dist/repl.d.ts.map +1 -1
  38. package/dist/repl.js +45 -34
  39. package/dist/repl.js.map +1 -1
  40. package/dist/ui/format.d.ts.map +1 -1
  41. package/dist/ui/format.js +2 -0
  42. package/dist/ui/format.js.map +1 -1
  43. package/package.json +1 -1
  44. package/src/commands/backup.ts +117 -0
  45. package/src/commands/config.ts +12 -2
  46. package/src/commands/discover.ts +74 -21
  47. package/src/commands/doctor.ts +172 -0
  48. package/src/commands/wallet.ts +194 -57
  49. package/src/commands/watch.ts +207 -10
  50. package/src/config.ts +48 -2
  51. package/src/daemon/index.ts +297 -152
  52. package/src/endpoint-notify.ts +86 -3
  53. package/src/index.ts +26 -0
  54. package/src/program.ts +4 -0
  55. package/src/repl.ts +53 -42
  56. package/src/ui/format.ts +1 -0
@@ -52,6 +52,7 @@ 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");
56
57
  const wallet_monitor_1 = require("./wallet-monitor");
57
58
  const notify_1 = require("./notify");
@@ -144,6 +145,40 @@ function openStateDB(dbPath) {
144
145
  },
145
146
  };
146
147
  }
148
+ // ─── Auth token ───────────────────────────────────────────────────────────────
149
+ const DAEMON_TOKEN_FILE = path.join(path.dirname(config_1.DAEMON_SOCK), "daemon.token");
150
+ function generateApiToken() {
151
+ return crypto.randomBytes(32).toString("hex");
152
+ }
153
+ function saveApiToken(token) {
154
+ fs.mkdirSync(path.dirname(DAEMON_TOKEN_FILE), { recursive: true, mode: 0o700 });
155
+ fs.writeFileSync(DAEMON_TOKEN_FILE, token, { mode: 0o600 });
156
+ }
157
+ function loadApiToken() {
158
+ try {
159
+ return fs.readFileSync(DAEMON_TOKEN_FILE, "utf-8").trim();
160
+ }
161
+ catch {
162
+ return null;
163
+ }
164
+ }
165
+ const rateLimitMap = new Map();
166
+ const RATE_LIMIT = 30;
167
+ const RATE_WINDOW_MS = 60000;
168
+ function checkRateLimit(ip) {
169
+ const now = Date.now();
170
+ let bucket = rateLimitMap.get(ip);
171
+ if (!bucket || now >= bucket.resetTime) {
172
+ bucket = { count: 0, resetTime: now + RATE_WINDOW_MS };
173
+ rateLimitMap.set(ip, bucket);
174
+ }
175
+ bucket.count++;
176
+ return bucket.count <= RATE_LIMIT;
177
+ }
178
+ // Cleanup stale rate limit entries every 5 minutes to prevent unbounded growth
179
+ let rateLimitCleanupInterval = null;
180
+ // ─── Body size limit ──────────────────────────────────────────────────────────
181
+ const MAX_BODY_SIZE = 1024 * 1024; // 1 MB
147
182
  // ─── Logger ───────────────────────────────────────────────────────────────────
148
183
  function openLogger(logPath, foreground) {
149
184
  let stream = null;
@@ -161,13 +196,14 @@ function openLogger(logPath, foreground) {
161
196
  }
162
197
  };
163
198
  }
164
- function startIpcServer(ctx, log) {
199
+ function startIpcServer(ctx, log, apiToken) {
165
200
  // Remove stale socket
166
201
  if (fs.existsSync(config_1.DAEMON_SOCK)) {
167
202
  fs.unlinkSync(config_1.DAEMON_SOCK);
168
203
  }
169
204
  const server = net.createServer((socket) => {
170
205
  let buf = "";
206
+ let authenticated = false;
171
207
  socket.on("data", (data) => {
172
208
  buf += data.toString();
173
209
  const lines = buf.split("\n");
@@ -183,6 +219,19 @@ function startIpcServer(ctx, log) {
183
219
  socket.write(JSON.stringify({ ok: false, error: "invalid_json" }) + "\n");
184
220
  continue;
185
221
  }
222
+ // First message must be auth
223
+ if (!authenticated) {
224
+ if (cmd.auth === apiToken) {
225
+ authenticated = true;
226
+ socket.write(JSON.stringify({ ok: true, authenticated: true }) + "\n");
227
+ }
228
+ else {
229
+ log({ event: "ipc_auth_failed" });
230
+ socket.write(JSON.stringify({ ok: false, error: "unauthorized" }) + "\n");
231
+ socket.destroy();
232
+ }
233
+ continue;
234
+ }
186
235
  const response = handleIpcCommand(cmd, ctx, log);
187
236
  socket.write(JSON.stringify(response) + "\n");
188
237
  }
@@ -464,6 +513,14 @@ async function runDaemon(foreground = false) {
464
513
  }
465
514
  }
466
515
  }, 30000);
516
+ // Rate limit map cleanup — every 5 minutes (prevents unbounded growth)
517
+ rateLimitCleanupInterval = setInterval(() => {
518
+ const now = Date.now();
519
+ for (const [ip, bucket] of rateLimitMap) {
520
+ if (bucket.resetTime < now)
521
+ rateLimitMap.delete(ip);
522
+ }
523
+ }, 5 * 60 * 1000);
467
524
  // Balance monitor — every 5 minutes
468
525
  const balanceInterval = setInterval(async () => {
469
526
  try {
@@ -481,15 +538,76 @@ async function runDaemon(foreground = false) {
481
538
  fs.writeFileSync(config_1.DAEMON_PID, String(process.pid), { mode: 0o600 });
482
539
  log({ event: "pid_written", pid: process.pid, path: config_1.DAEMON_PID });
483
540
  }
541
+ // ── Generate and save API token ──────────────────────────────────────────
542
+ const apiToken = generateApiToken();
543
+ saveApiToken(apiToken);
544
+ log({ event: "auth_token_saved", path: DAEMON_TOKEN_FILE });
484
545
  // ── Start IPC socket ─────────────────────────────────────────────────────
485
- const ipcServer = startIpcServer(ipcCtx, log);
546
+ const ipcServer = startIpcServer(ipcCtx, log, apiToken);
486
547
  // ── Start HTTP relay server (public endpoint) ────────────────────────────
487
548
  const httpPort = config.relay.listen_port ?? 4402;
549
+ /**
550
+ * Optionally verifies X-ARC402-Signature against the request body.
551
+ * Logs the result but never rejects — unsigned requests are accepted for backwards compat.
552
+ */
553
+ function verifyRequestSignature(body, req) {
554
+ const sig = req.headers["x-arc402-signature"];
555
+ if (!sig)
556
+ return;
557
+ const claimedSigner = req.headers["x-arc402-signer"];
558
+ try {
559
+ const recovered = ethers_1.ethers.verifyMessage(body, sig);
560
+ if (claimedSigner && recovered.toLowerCase() !== claimedSigner.toLowerCase()) {
561
+ log({ event: "sig_mismatch", claimed: claimedSigner, recovered });
562
+ }
563
+ else {
564
+ log({ event: "sig_verified", signer: recovered });
565
+ }
566
+ }
567
+ catch {
568
+ log({ event: "sig_invalid" });
569
+ }
570
+ }
571
+ /**
572
+ * Read request body with a size cap. Destroys the request and sends 413
573
+ * if the body exceeds MAX_BODY_SIZE. Returns null in that case.
574
+ */
575
+ function readBody(req, res) {
576
+ return new Promise((resolve) => {
577
+ let body = "";
578
+ let size = 0;
579
+ req.on("data", (chunk) => {
580
+ size += chunk.length;
581
+ if (size > MAX_BODY_SIZE) {
582
+ req.destroy();
583
+ res.writeHead(413, { "Content-Type": "application/json" });
584
+ res.end(JSON.stringify({ error: "payload_too_large" }));
585
+ resolve(null);
586
+ return;
587
+ }
588
+ body += chunk.toString();
589
+ });
590
+ req.on("end", () => { resolve(body); });
591
+ req.on("error", () => { resolve(null); });
592
+ });
593
+ }
594
+ const PUBLIC_GET_PATHS = new Set(["/", "/health", "/agent", "/capabilities", "/status"]);
595
+ // CORS whitelist — localhost for local tooling, arc402.xyz for the web app
596
+ const CORS_WHITELIST = new Set(["localhost", "127.0.0.1", "arc402.xyz", "app.arc402.xyz"]);
488
597
  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");
598
+ // CORS — only reflect origin header if it's in the whitelist
599
+ const origin = (req.headers["origin"] ?? "");
600
+ if (origin) {
601
+ try {
602
+ const { hostname } = new URL(origin);
603
+ if (CORS_WHITELIST.has(hostname)) {
604
+ res.setHeader("Access-Control-Allow-Origin", origin);
605
+ res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
606
+ res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
607
+ }
608
+ }
609
+ catch { /* ignore invalid origin */ }
610
+ }
493
611
  if (req.method === "OPTIONS") {
494
612
  res.writeHead(204);
495
613
  res.end();
@@ -497,6 +615,25 @@ async function runDaemon(foreground = false) {
497
615
  }
498
616
  const url = new URL(req.url || "/", `http://localhost:${httpPort}`);
499
617
  const pathname = url.pathname;
618
+ // Rate limiting (all endpoints)
619
+ const clientIp = (req.socket.remoteAddress ?? "unknown").replace(/^::ffff:/, "");
620
+ if (!checkRateLimit(clientIp)) {
621
+ log({ event: "rate_limited", ip: clientIp, path: pathname });
622
+ res.writeHead(429, { "Content-Type": "application/json" });
623
+ res.end(JSON.stringify({ error: "too_many_requests" }));
624
+ return;
625
+ }
626
+ // Auth required on all POST endpoints (GET public paths are open)
627
+ if (req.method === "POST" || (req.method === "GET" && !PUBLIC_GET_PATHS.has(pathname))) {
628
+ const authHeader = req.headers["authorization"] ?? "";
629
+ const token = authHeader.startsWith("Bearer ") ? authHeader.slice(7) : "";
630
+ if (token !== apiToken) {
631
+ log({ event: "http_unauthorized", ip: clientIp, path: pathname });
632
+ res.writeHead(401, { "Content-Type": "application/json" });
633
+ res.end(JSON.stringify({ error: "unauthorized" }));
634
+ return;
635
+ }
636
+ }
500
637
  // Health / info
501
638
  if (pathname === "/" || pathname === "/health") {
502
639
  const info = {
@@ -526,51 +663,36 @@ async function runDaemon(foreground = false) {
526
663
  }
527
664
  // Receive hire proposal
528
665
  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";
666
+ const body = await readBody(req, res);
667
+ if (body === null)
668
+ return;
669
+ verifyRequestSignature(body, req);
670
+ try {
671
+ const msg = JSON.parse(body);
672
+ // Feed into the hire listener's message handler
673
+ const proposal = {
674
+ messageId: String(msg.messageId ?? msg.id ?? `http_${Date.now()}`),
675
+ hirerAddress: String(msg.hirerAddress ?? msg.hirer_address ?? msg.from ?? ""),
676
+ capability: String(msg.capability ?? ""),
677
+ priceEth: String(msg.priceEth ?? msg.price_eth ?? "0"),
678
+ deadlineUnix: Number(msg.deadlineUnix ?? msg.deadline ?? 0),
679
+ specHash: String(msg.specHash ?? msg.spec_hash ?? ""),
680
+ agreementId: msg.agreementId ? String(msg.agreementId) : undefined,
681
+ signature: msg.signature ? String(msg.signature) : undefined,
682
+ };
683
+ // Dedup
684
+ const existing = db.getHireRequest(proposal.messageId);
685
+ if (existing) {
686
+ log({ event: "hire_duplicate", id: proposal.messageId, status: existing.status });
687
+ res.writeHead(200, { "Content-Type": "application/json" });
688
+ res.end(JSON.stringify({ status: existing.status, id: proposal.messageId }));
689
+ return;
690
+ }
691
+ // Policy check
692
+ const { evaluatePolicy } = await Promise.resolve().then(() => __importStar(require("./hire-listener")));
693
+ const activeCount = db.countActiveHireRequests();
694
+ const policyResult = evaluatePolicy(proposal, config, activeCount);
695
+ if (!policyResult.allowed) {
574
696
  db.insertHireRequest({
575
697
  id: proposal.messageId,
576
698
  agreement_id: proposal.agreementId ?? null,
@@ -579,206 +701,217 @@ async function runDaemon(foreground = false) {
579
701
  price_eth: proposal.priceEth,
580
702
  deadline_unix: proposal.deadlineUnix,
581
703
  spec_hash: proposal.specHash,
582
- status,
583
- reject_reason: null,
704
+ status: "rejected",
705
+ reject_reason: policyResult.reason ?? "policy_violation",
584
706
  });
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
- }
707
+ log({ event: "http_hire_rejected", id: proposal.messageId, reason: policyResult.reason });
589
708
  res.writeHead(200, { "Content-Type": "application/json" });
590
- res.end(JSON.stringify({ status, id: proposal.messageId }));
709
+ res.end(JSON.stringify({ status: "rejected", reason: policyResult.reason, id: proposal.messageId }));
710
+ return;
591
711
  }
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" }));
712
+ const status = config.policy.auto_accept ? "accepted" : "pending_approval";
713
+ db.insertHireRequest({
714
+ id: proposal.messageId,
715
+ agreement_id: proposal.agreementId ?? null,
716
+ hirer_address: proposal.hirerAddress,
717
+ capability: proposal.capability,
718
+ price_eth: proposal.priceEth,
719
+ deadline_unix: proposal.deadlineUnix,
720
+ spec_hash: proposal.specHash,
721
+ status,
722
+ reject_reason: null,
723
+ });
724
+ log({ event: "http_hire_received", id: proposal.messageId, hirer: proposal.hirerAddress, status });
725
+ if (config.notifications.notify_on_hire_request) {
726
+ await notifier.notifyHireRequest(proposal.messageId, proposal.hirerAddress, proposal.priceEth, proposal.capability);
596
727
  }
597
- });
728
+ res.writeHead(200, { "Content-Type": "application/json" });
729
+ res.end(JSON.stringify({ status, id: proposal.messageId }));
730
+ }
731
+ catch (err) {
732
+ log({ event: "http_hire_error", error: String(err) });
733
+ res.writeHead(400, { "Content-Type": "application/json" });
734
+ res.end(JSON.stringify({ error: "invalid_request" }));
735
+ }
598
736
  return;
599
737
  }
600
738
  // Handshake acknowledgment endpoint
601
739
  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
- });
740
+ const body = await readBody(req, res);
741
+ if (body === null)
742
+ return;
743
+ verifyRequestSignature(body, req);
744
+ try {
745
+ const msg = JSON.parse(body);
746
+ log({ event: "handshake_received", from: msg.from, type: msg.type, note: msg.note });
747
+ res.writeHead(200, { "Content-Type": "application/json" });
748
+ res.end(JSON.stringify({ received: true, agent: config.wallet.contract_address }));
749
+ }
750
+ catch {
751
+ res.writeHead(400, { "Content-Type": "application/json" });
752
+ res.end(JSON.stringify({ error: "invalid_request" }));
753
+ }
616
754
  return;
617
755
  }
618
756
  // POST /hire/accepted — provider accepted, client notified
619
757
  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 }));
758
+ const body = await readBody(req, res);
759
+ if (body === null)
760
+ return;
761
+ try {
762
+ const msg = JSON.parse(body);
763
+ const agreementId = String(msg.agreementId ?? msg.agreement_id ?? "");
764
+ const from = String(msg.from ?? "");
765
+ log({ event: "hire_accepted_inbound", agreementId, from });
766
+ if (config.notifications.notify_on_hire_accepted) {
767
+ await notifier.notifyHireAccepted(agreementId, agreementId);
633
768
  }
634
- catch {
635
- res.writeHead(400, { "Content-Type": "application/json" });
636
- res.end(JSON.stringify({ error: "invalid_request" }));
637
- }
638
- });
769
+ res.writeHead(200, { "Content-Type": "application/json" });
770
+ res.end(JSON.stringify({ received: true, agent: config.wallet.contract_address }));
771
+ }
772
+ catch {
773
+ res.writeHead(400, { "Content-Type": "application/json" });
774
+ res.end(JSON.stringify({ error: "invalid_request" }));
775
+ }
639
776
  return;
640
777
  }
641
778
  // POST /message — off-chain negotiation message
642
779
  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
- });
780
+ const body = await readBody(req, res);
781
+ if (body === null)
782
+ return;
783
+ verifyRequestSignature(body, req);
784
+ try {
785
+ const msg = JSON.parse(body);
786
+ const from = String(msg.from ?? "");
787
+ const to = String(msg.to ?? "");
788
+ const content = String(msg.content ?? "");
789
+ const timestamp = Number(msg.timestamp ?? Date.now());
790
+ log({ event: "message_received", from, to, timestamp, content_len: content.length });
791
+ res.writeHead(200, { "Content-Type": "application/json" });
792
+ res.end(JSON.stringify({ received: true, agent: config.wallet.contract_address }));
793
+ }
794
+ catch {
795
+ res.writeHead(400, { "Content-Type": "application/json" });
796
+ res.end(JSON.stringify({ error: "invalid_request" }));
797
+ }
661
798
  return;
662
799
  }
663
800
  // POST /delivery — provider committed a deliverable
664
801
  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 }));
684
- }
685
- catch {
686
- res.writeHead(400, { "Content-Type": "application/json" });
687
- res.end(JSON.stringify({ error: "invalid_request" }));
802
+ const body = await readBody(req, res);
803
+ if (body === null)
804
+ return;
805
+ verifyRequestSignature(body, req);
806
+ try {
807
+ const msg = JSON.parse(body);
808
+ const agreementId = String(msg.agreementId ?? msg.agreement_id ?? "");
809
+ const deliverableHash = String(msg.deliverableHash ?? msg.deliverable_hash ?? "");
810
+ const from = String(msg.from ?? "");
811
+ log({ event: "delivery_received", agreementId, deliverableHash, from });
812
+ // Update DB: mark delivered
813
+ const active = db.listActiveHireRequests();
814
+ const found = active.find(r => r.agreement_id === agreementId);
815
+ if (found)
816
+ db.updateHireRequestStatus(found.id, "delivered");
817
+ if (config.notifications.notify_on_delivery) {
818
+ await notifier.notifyDelivery(agreementId, deliverableHash, "");
688
819
  }
689
- });
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
+ }
690
827
  return;
691
828
  }
692
829
  // POST /delivery/accepted — client accepted delivery, payment releasing
693
830
  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
- });
831
+ const body = await readBody(req, res);
832
+ if (body === null)
833
+ return;
834
+ try {
835
+ const msg = JSON.parse(body);
836
+ const agreementId = String(msg.agreementId ?? msg.agreement_id ?? "");
837
+ const from = String(msg.from ?? "");
838
+ log({ event: "delivery_accepted_inbound", agreementId, from });
839
+ // Update DB: mark complete
840
+ const all = db.listActiveHireRequests();
841
+ const found = all.find(r => r.agreement_id === agreementId);
842
+ if (found)
843
+ db.updateHireRequestStatus(found.id, "complete");
844
+ res.writeHead(200, { "Content-Type": "application/json" });
845
+ res.end(JSON.stringify({ received: true, agent: config.wallet.contract_address }));
846
+ }
847
+ catch {
848
+ res.writeHead(400, { "Content-Type": "application/json" });
849
+ res.end(JSON.stringify({ error: "invalid_request" }));
850
+ }
715
851
  return;
716
852
  }
717
853
  // POST /dispute — dispute raised against this agent
718
854
  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 }));
733
- }
734
- catch {
735
- res.writeHead(400, { "Content-Type": "application/json" });
736
- res.end(JSON.stringify({ error: "invalid_request" }));
855
+ const body = await readBody(req, res);
856
+ if (body === null)
857
+ return;
858
+ try {
859
+ const msg = JSON.parse(body);
860
+ const agreementId = String(msg.agreementId ?? msg.agreement_id ?? "");
861
+ const reason = String(msg.reason ?? "");
862
+ const from = String(msg.from ?? "");
863
+ log({ event: "dispute_received", agreementId, reason, from });
864
+ if (config.notifications.notify_on_dispute) {
865
+ await notifier.notifyDispute(agreementId, from);
737
866
  }
738
- });
867
+ res.writeHead(200, { "Content-Type": "application/json" });
868
+ res.end(JSON.stringify({ received: true, agent: config.wallet.contract_address }));
869
+ }
870
+ catch {
871
+ res.writeHead(400, { "Content-Type": "application/json" });
872
+ res.end(JSON.stringify({ error: "invalid_request" }));
873
+ }
739
874
  return;
740
875
  }
741
876
  // POST /dispute/resolved — dispute resolved by arbitrator
742
877
  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
- });
878
+ const body = await readBody(req, res);
879
+ if (body === null)
880
+ return;
881
+ try {
882
+ const msg = JSON.parse(body);
883
+ const agreementId = String(msg.agreementId ?? msg.agreement_id ?? "");
884
+ const outcome = String(msg.outcome ?? "");
885
+ const from = String(msg.from ?? "");
886
+ log({ event: "dispute_resolved_inbound", agreementId, outcome, from });
887
+ res.writeHead(200, { "Content-Type": "application/json" });
888
+ res.end(JSON.stringify({ received: true, agent: config.wallet.contract_address }));
889
+ }
890
+ catch {
891
+ res.writeHead(400, { "Content-Type": "application/json" });
892
+ res.end(JSON.stringify({ error: "invalid_request" }));
893
+ }
760
894
  return;
761
895
  }
762
896
  // POST /workroom/status — workroom lifecycle events
763
897
  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
- });
898
+ const body = await readBody(req, res);
899
+ if (body === null)
900
+ return;
901
+ try {
902
+ const msg = JSON.parse(body);
903
+ const event = String(msg.event ?? "");
904
+ const agentAddress = String(msg.agentAddress ?? config.wallet.contract_address);
905
+ const jobId = msg.jobId ? String(msg.jobId) : undefined;
906
+ const timestamp = Number(msg.timestamp ?? Date.now());
907
+ log({ event: "workroom_lifecycle", workroom_event: event, agentAddress, jobId, timestamp });
908
+ res.writeHead(200, { "Content-Type": "application/json" });
909
+ res.end(JSON.stringify({ received: true, agent: config.wallet.contract_address }));
910
+ }
911
+ catch {
912
+ res.writeHead(400, { "Content-Type": "application/json" });
913
+ res.end(JSON.stringify({ error: "invalid_request" }));
914
+ }
782
915
  return;
783
916
  }
784
917
  // GET /capabilities — agent capabilities from config
@@ -792,21 +925,25 @@ async function runDaemon(foreground = false) {
792
925
  }));
793
926
  return;
794
927
  }
795
- // GET /status — health with active agreement count
928
+ // GET /status — health with active agreement count (sensitive counts only for authenticated)
796
929
  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({
930
+ const statusAuth = (req.headers["authorization"] ?? "");
931
+ const statusToken = statusAuth.startsWith("Bearer ") ? statusAuth.slice(7) : "";
932
+ const statusAuthed = statusToken === apiToken;
933
+ const statusPayload = {
801
934
  protocol: "arc-402",
802
935
  version: "0.3.0",
803
936
  agent: config.wallet.contract_address,
804
937
  status: "online",
805
938
  uptime: Math.floor((Date.now() - ipcCtx.startTime) / 1000),
806
- active_agreements: activeList.length,
807
- pending_approval: pendingList.length,
808
939
  capabilities: config.policy.allowed_capabilities,
809
- }));
940
+ };
941
+ if (statusAuthed) {
942
+ statusPayload.active_agreements = db.listActiveHireRequests().length;
943
+ statusPayload.pending_approval = db.listPendingHireRequests().length;
944
+ }
945
+ res.writeHead(200, { "Content-Type": "application/json" });
946
+ res.end(JSON.stringify(statusPayload));
810
947
  return;
811
948
  }
812
949
  // 404
@@ -838,6 +975,8 @@ async function runDaemon(foreground = false) {
838
975
  clearInterval(relayInterval);
839
976
  clearInterval(timeoutInterval);
840
977
  clearInterval(balanceInterval);
978
+ if (rateLimitCleanupInterval)
979
+ clearInterval(rateLimitCleanupInterval);
841
980
  // Close HTTP + IPC
842
981
  httpServer.close();
843
982
  ipcServer.close();