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
@@ -14,6 +14,8 @@ import * as http from "http";
14
14
  import { ethers } from "ethers";
15
15
  import Database from "better-sqlite3";
16
16
 
17
+ import * as crypto from "crypto";
18
+
17
19
  import {
18
20
  loadDaemonConfig,
19
21
  loadMachineKey,
@@ -147,6 +149,52 @@ function openStateDB(dbPath: string): DaemonDB {
147
149
  };
148
150
  }
149
151
 
152
+ // ─── Auth token ───────────────────────────────────────────────────────────────
153
+
154
+ const DAEMON_TOKEN_FILE = path.join(path.dirname(DAEMON_SOCK), "daemon.token");
155
+
156
+ function generateApiToken(): string {
157
+ return crypto.randomBytes(32).toString("hex");
158
+ }
159
+
160
+ function saveApiToken(token: string): void {
161
+ fs.mkdirSync(path.dirname(DAEMON_TOKEN_FILE), { recursive: true, mode: 0o700 });
162
+ fs.writeFileSync(DAEMON_TOKEN_FILE, token, { mode: 0o600 });
163
+ }
164
+
165
+ function loadApiToken(): string | null {
166
+ try {
167
+ return fs.readFileSync(DAEMON_TOKEN_FILE, "utf-8").trim();
168
+ } catch {
169
+ return null;
170
+ }
171
+ }
172
+
173
+ // ─── Rate limiter ─────────────────────────────────────────────────────────────
174
+
175
+ interface RateBucket { count: number; resetTime: number }
176
+ const rateLimitMap = new Map<string, RateBucket>();
177
+ const RATE_LIMIT = 30;
178
+ const RATE_WINDOW_MS = 60_000;
179
+
180
+ function checkRateLimit(ip: string): boolean {
181
+ const now = Date.now();
182
+ let bucket = rateLimitMap.get(ip);
183
+ if (!bucket || now >= bucket.resetTime) {
184
+ bucket = { count: 0, resetTime: now + RATE_WINDOW_MS };
185
+ rateLimitMap.set(ip, bucket);
186
+ }
187
+ bucket.count++;
188
+ return bucket.count <= RATE_LIMIT;
189
+ }
190
+
191
+ // Cleanup stale rate limit entries every 5 minutes to prevent unbounded growth
192
+ let rateLimitCleanupInterval: ReturnType<typeof setInterval> | null = null;
193
+
194
+ // ─── Body size limit ──────────────────────────────────────────────────────────
195
+
196
+ const MAX_BODY_SIZE = 1024 * 1024; // 1 MB
197
+
150
198
  // ─── Logger ───────────────────────────────────────────────────────────────────
151
199
 
152
200
  function openLogger(logPath: string, foreground: boolean): (entry: Record<string, unknown>) => void {
@@ -181,7 +229,7 @@ interface IpcContext {
181
229
  bundlerEndpoint: string;
182
230
  }
183
231
 
184
- function startIpcServer(ctx: IpcContext, log: ReturnType<typeof openLogger>): net.Server {
232
+ function startIpcServer(ctx: IpcContext, log: ReturnType<typeof openLogger>, apiToken: string): net.Server {
185
233
  // Remove stale socket
186
234
  if (fs.existsSync(DAEMON_SOCK)) {
187
235
  fs.unlinkSync(DAEMON_SOCK);
@@ -189,20 +237,34 @@ function startIpcServer(ctx: IpcContext, log: ReturnType<typeof openLogger>): ne
189
237
 
190
238
  const server = net.createServer((socket) => {
191
239
  let buf = "";
240
+ let authenticated = false;
192
241
  socket.on("data", (data) => {
193
242
  buf += data.toString();
194
243
  const lines = buf.split("\n");
195
244
  buf = lines.pop() ?? "";
196
245
  for (const line of lines) {
197
246
  if (!line.trim()) continue;
198
- let cmd: { command: string; id?: string; reason?: string };
247
+ let cmd: { command: string; id?: string; reason?: string; auth?: string };
199
248
  try {
200
- cmd = JSON.parse(line) as { command: string; id?: string; reason?: string };
249
+ cmd = JSON.parse(line) as { command: string; id?: string; reason?: string; auth?: string };
201
250
  } catch {
202
251
  socket.write(JSON.stringify({ ok: false, error: "invalid_json" }) + "\n");
203
252
  continue;
204
253
  }
205
254
 
255
+ // First message must be auth
256
+ if (!authenticated) {
257
+ if (cmd.auth === apiToken) {
258
+ authenticated = true;
259
+ socket.write(JSON.stringify({ ok: true, authenticated: true }) + "\n");
260
+ } else {
261
+ log({ event: "ipc_auth_failed" });
262
+ socket.write(JSON.stringify({ ok: false, error: "unauthorized" }) + "\n");
263
+ socket.destroy();
264
+ }
265
+ continue;
266
+ }
267
+
206
268
  const response = handleIpcCommand(cmd, ctx, log);
207
269
  socket.write(JSON.stringify(response) + "\n");
208
270
  }
@@ -529,6 +591,14 @@ export async function runDaemon(foreground = false): Promise<void> {
529
591
  }
530
592
  }, 30_000);
531
593
 
594
+ // Rate limit map cleanup — every 5 minutes (prevents unbounded growth)
595
+ rateLimitCleanupInterval = setInterval(() => {
596
+ const now = Date.now();
597
+ for (const [ip, bucket] of rateLimitMap) {
598
+ if (bucket.resetTime < now) rateLimitMap.delete(ip);
599
+ }
600
+ }, 5 * 60 * 1000);
601
+
532
602
  // Balance monitor — every 5 minutes
533
603
  const balanceInterval = setInterval(async () => {
534
604
  try {
@@ -547,22 +617,105 @@ export async function runDaemon(foreground = false): Promise<void> {
547
617
  log({ event: "pid_written", pid: process.pid, path: DAEMON_PID });
548
618
  }
549
619
 
620
+ // ── Generate and save API token ──────────────────────────────────────────
621
+ const apiToken = generateApiToken();
622
+ saveApiToken(apiToken);
623
+ log({ event: "auth_token_saved", path: DAEMON_TOKEN_FILE });
624
+
550
625
  // ── Start IPC socket ─────────────────────────────────────────────────────
551
- const ipcServer = startIpcServer(ipcCtx, log);
626
+ const ipcServer = startIpcServer(ipcCtx, log, apiToken);
552
627
 
553
628
  // ── Start HTTP relay server (public endpoint) ────────────────────────────
554
629
  const httpPort = config.relay.listen_port ?? 4402;
555
630
 
631
+ /**
632
+ * Optionally verifies X-ARC402-Signature against the request body.
633
+ * Logs the result but never rejects — unsigned requests are accepted for backwards compat.
634
+ */
635
+ function verifyRequestSignature(body: string, req: http.IncomingMessage): void {
636
+ const sig = req.headers["x-arc402-signature"] as string | undefined;
637
+ if (!sig) return;
638
+ const claimedSigner = req.headers["x-arc402-signer"] as string | undefined;
639
+ try {
640
+ const recovered = ethers.verifyMessage(body, sig);
641
+ if (claimedSigner && recovered.toLowerCase() !== claimedSigner.toLowerCase()) {
642
+ log({ event: "sig_mismatch", claimed: claimedSigner, recovered });
643
+ } else {
644
+ log({ event: "sig_verified", signer: recovered });
645
+ }
646
+ } catch {
647
+ log({ event: "sig_invalid" });
648
+ }
649
+ }
650
+
651
+ /**
652
+ * Read request body with a size cap. Destroys the request and sends 413
653
+ * if the body exceeds MAX_BODY_SIZE. Returns null in that case.
654
+ */
655
+ function readBody(req: http.IncomingMessage, res: http.ServerResponse): Promise<string | null> {
656
+ return new Promise((resolve) => {
657
+ let body = "";
658
+ let size = 0;
659
+ req.on("data", (chunk: Buffer) => {
660
+ size += chunk.length;
661
+ if (size > MAX_BODY_SIZE) {
662
+ req.destroy();
663
+ res.writeHead(413, { "Content-Type": "application/json" });
664
+ res.end(JSON.stringify({ error: "payload_too_large" }));
665
+ resolve(null);
666
+ return;
667
+ }
668
+ body += chunk.toString();
669
+ });
670
+ req.on("end", () => { resolve(body); });
671
+ req.on("error", () => { resolve(null); });
672
+ });
673
+ }
674
+
675
+ const PUBLIC_GET_PATHS = new Set(["/", "/health", "/agent", "/capabilities", "/status"]);
676
+
677
+ // CORS whitelist — localhost for local tooling, arc402.xyz for the web app
678
+ const CORS_WHITELIST = new Set(["localhost", "127.0.0.1", "arc402.xyz", "app.arc402.xyz"]);
679
+
556
680
  const httpServer = http.createServer(async (req, res) => {
557
- // CORS headers
558
- res.setHeader("Access-Control-Allow-Origin", "*");
559
- res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
560
- res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
681
+ // CORS — only reflect origin header if it's in the whitelist
682
+ const origin = (req.headers["origin"] ?? "") as string;
683
+ if (origin) {
684
+ try {
685
+ const { hostname } = new URL(origin);
686
+ if (CORS_WHITELIST.has(hostname)) {
687
+ res.setHeader("Access-Control-Allow-Origin", origin);
688
+ res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
689
+ res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
690
+ }
691
+ } catch { /* ignore invalid origin */ }
692
+ }
561
693
  if (req.method === "OPTIONS") { res.writeHead(204); res.end(); return; }
562
694
 
563
695
  const url = new URL(req.url || "/", `http://localhost:${httpPort}`);
564
696
  const pathname = url.pathname;
565
697
 
698
+ // Rate limiting (all endpoints)
699
+ const clientIp = (req.socket.remoteAddress ?? "unknown").replace(/^::ffff:/, "");
700
+ if (!checkRateLimit(clientIp)) {
701
+ log({ event: "rate_limited", ip: clientIp, path: pathname });
702
+ res.writeHead(429, { "Content-Type": "application/json" });
703
+ res.end(JSON.stringify({ error: "too_many_requests" }));
704
+ return;
705
+ }
706
+
707
+ // Auth required on all POST endpoints (GET public paths are open)
708
+ if (req.method === "POST" || (req.method === "GET" && !PUBLIC_GET_PATHS.has(pathname))) {
709
+ const authHeader = req.headers["authorization"] ?? "";
710
+ const token = authHeader.startsWith("Bearer ") ? authHeader.slice(7) : "";
711
+ if (token !== apiToken) {
712
+ log({ event: "http_unauthorized", ip: clientIp, path: pathname });
713
+ res.writeHead(401, { "Content-Type": "application/json" });
714
+ res.end(JSON.stringify({ error: "unauthorized" }));
715
+ return;
716
+ }
717
+ }
718
+
566
719
  // Health / info
567
720
  if (pathname === "/" || pathname === "/health") {
568
721
  const info = {
@@ -594,10 +747,10 @@ export async function runDaemon(foreground = false): Promise<void> {
594
747
 
595
748
  // Receive hire proposal
596
749
  if (pathname === "/hire" && req.method === "POST") {
597
- let body = "";
598
- req.on("data", (chunk: Buffer) => { body += chunk.toString(); });
599
- req.on("end", async () => {
600
- try {
750
+ const body = await readBody(req, res);
751
+ if (body === null) return;
752
+ verifyRequestSignature(body, req);
753
+ try {
601
754
  const msg = JSON.parse(body) as Record<string, unknown>;
602
755
 
603
756
  // Feed into the hire listener's message handler
@@ -615,6 +768,7 @@ export async function runDaemon(foreground = false): Promise<void> {
615
768
  // Dedup
616
769
  const existing = db.getHireRequest(proposal.messageId);
617
770
  if (existing) {
771
+ log({ event: "hire_duplicate", id: proposal.messageId, status: existing.status });
618
772
  res.writeHead(200, { "Content-Type": "application/json" });
619
773
  res.end(JSON.stringify({ status: existing.status, id: proposal.messageId }));
620
774
  return;
@@ -669,16 +823,15 @@ export async function runDaemon(foreground = false): Promise<void> {
669
823
  res.writeHead(400, { "Content-Type": "application/json" });
670
824
  res.end(JSON.stringify({ error: "invalid_request" }));
671
825
  }
672
- });
673
826
  return;
674
827
  }
675
828
 
676
829
  // Handshake acknowledgment endpoint
677
830
  if (pathname === "/handshake" && req.method === "POST") {
678
- let body = "";
679
- req.on("data", (chunk: Buffer) => { body += chunk.toString(); });
680
- req.on("end", () => {
681
- try {
831
+ const body = await readBody(req, res);
832
+ if (body === null) return;
833
+ verifyRequestSignature(body, req);
834
+ try {
682
835
  const msg = JSON.parse(body);
683
836
  log({ event: "handshake_received", from: msg.from, type: msg.type, note: msg.note });
684
837
  res.writeHead(200, { "Content-Type": "application/json" });
@@ -687,171 +840,158 @@ export async function runDaemon(foreground = false): Promise<void> {
687
840
  res.writeHead(400, { "Content-Type": "application/json" });
688
841
  res.end(JSON.stringify({ error: "invalid_request" }));
689
842
  }
690
- });
691
843
  return;
692
844
  }
693
845
 
694
846
  // POST /hire/accepted — provider accepted, client notified
695
847
  if (pathname === "/hire/accepted" && req.method === "POST") {
696
- let body = "";
697
- req.on("data", (chunk: Buffer) => { body += chunk.toString(); });
698
- req.on("end", async () => {
699
- try {
700
- const msg = JSON.parse(body) as Record<string, unknown>;
701
- const agreementId = String(msg.agreementId ?? msg.agreement_id ?? "");
702
- const from = String(msg.from ?? "");
703
- log({ event: "hire_accepted_inbound", agreementId, from });
704
- if (config.notifications.notify_on_hire_accepted) {
705
- await notifier.notifyHireAccepted(agreementId, agreementId);
706
- }
707
- res.writeHead(200, { "Content-Type": "application/json" });
708
- res.end(JSON.stringify({ received: true, agent: config.wallet.contract_address }));
709
- } catch {
710
- res.writeHead(400, { "Content-Type": "application/json" });
711
- res.end(JSON.stringify({ error: "invalid_request" }));
848
+ const body = await readBody(req, res);
849
+ if (body === null) return;
850
+ try {
851
+ const msg = JSON.parse(body) as Record<string, unknown>;
852
+ const agreementId = String(msg.agreementId ?? msg.agreement_id ?? "");
853
+ const from = String(msg.from ?? "");
854
+ log({ event: "hire_accepted_inbound", agreementId, from });
855
+ if (config.notifications.notify_on_hire_accepted) {
856
+ await notifier.notifyHireAccepted(agreementId, agreementId);
712
857
  }
713
- });
858
+ res.writeHead(200, { "Content-Type": "application/json" });
859
+ res.end(JSON.stringify({ received: true, agent: config.wallet.contract_address }));
860
+ } catch {
861
+ res.writeHead(400, { "Content-Type": "application/json" });
862
+ res.end(JSON.stringify({ error: "invalid_request" }));
863
+ }
714
864
  return;
715
865
  }
716
866
 
717
867
  // POST /message — off-chain negotiation message
718
868
  if (pathname === "/message" && req.method === "POST") {
719
- let body = "";
720
- req.on("data", (chunk: Buffer) => { body += chunk.toString(); });
721
- req.on("end", () => {
722
- try {
723
- const msg = JSON.parse(body) as Record<string, unknown>;
724
- const from = String(msg.from ?? "");
725
- const to = String(msg.to ?? "");
726
- const content = String(msg.content ?? "");
727
- const timestamp = Number(msg.timestamp ?? Date.now());
728
- log({ event: "message_received", from, to, timestamp, content_len: content.length });
729
- res.writeHead(200, { "Content-Type": "application/json" });
730
- res.end(JSON.stringify({ received: true, agent: config.wallet.contract_address }));
731
- } catch {
732
- res.writeHead(400, { "Content-Type": "application/json" });
733
- res.end(JSON.stringify({ error: "invalid_request" }));
734
- }
735
- });
869
+ const body = await readBody(req, res);
870
+ if (body === null) return;
871
+ verifyRequestSignature(body, req);
872
+ try {
873
+ const msg = JSON.parse(body) as Record<string, unknown>;
874
+ const from = String(msg.from ?? "");
875
+ const to = String(msg.to ?? "");
876
+ const content = String(msg.content ?? "");
877
+ const timestamp = Number(msg.timestamp ?? Date.now());
878
+ log({ event: "message_received", from, to, timestamp, content_len: content.length });
879
+ res.writeHead(200, { "Content-Type": "application/json" });
880
+ res.end(JSON.stringify({ received: true, agent: config.wallet.contract_address }));
881
+ } catch {
882
+ res.writeHead(400, { "Content-Type": "application/json" });
883
+ res.end(JSON.stringify({ error: "invalid_request" }));
884
+ }
736
885
  return;
737
886
  }
738
887
 
739
888
  // POST /delivery — provider committed a deliverable
740
889
  if (pathname === "/delivery" && req.method === "POST") {
741
- let body = "";
742
- req.on("data", (chunk: Buffer) => { body += chunk.toString(); });
743
- req.on("end", async () => {
744
- try {
745
- const msg = JSON.parse(body) as Record<string, unknown>;
746
- const agreementId = String(msg.agreementId ?? msg.agreement_id ?? "");
747
- const deliverableHash = String(msg.deliverableHash ?? msg.deliverable_hash ?? "");
748
- const from = String(msg.from ?? "");
749
- log({ event: "delivery_received", agreementId, deliverableHash, from });
750
- // Update DB: mark delivered
751
- const active = db.listActiveHireRequests();
752
- const found = active.find(r => r.agreement_id === agreementId);
753
- if (found) db.updateHireRequestStatus(found.id, "delivered");
754
- if (config.notifications.notify_on_delivery) {
755
- await notifier.notifyDelivery(agreementId, deliverableHash, "");
756
- }
757
- res.writeHead(200, { "Content-Type": "application/json" });
758
- res.end(JSON.stringify({ received: true, agent: config.wallet.contract_address }));
759
- } catch {
760
- res.writeHead(400, { "Content-Type": "application/json" });
761
- res.end(JSON.stringify({ error: "invalid_request" }));
890
+ const body = await readBody(req, res);
891
+ if (body === null) return;
892
+ verifyRequestSignature(body, req);
893
+ try {
894
+ const msg = JSON.parse(body) as Record<string, unknown>;
895
+ const agreementId = String(msg.agreementId ?? msg.agreement_id ?? "");
896
+ const deliverableHash = String(msg.deliverableHash ?? msg.deliverable_hash ?? "");
897
+ const from = String(msg.from ?? "");
898
+ log({ event: "delivery_received", agreementId, deliverableHash, from });
899
+ // Update DB: mark delivered
900
+ const active = db.listActiveHireRequests();
901
+ const found = active.find(r => r.agreement_id === agreementId);
902
+ if (found) db.updateHireRequestStatus(found.id, "delivered");
903
+ if (config.notifications.notify_on_delivery) {
904
+ await notifier.notifyDelivery(agreementId, deliverableHash, "");
762
905
  }
763
- });
906
+ res.writeHead(200, { "Content-Type": "application/json" });
907
+ res.end(JSON.stringify({ received: true, agent: config.wallet.contract_address }));
908
+ } catch {
909
+ res.writeHead(400, { "Content-Type": "application/json" });
910
+ res.end(JSON.stringify({ error: "invalid_request" }));
911
+ }
764
912
  return;
765
913
  }
766
914
 
767
915
  // POST /delivery/accepted — client accepted delivery, payment releasing
768
916
  if (pathname === "/delivery/accepted" && req.method === "POST") {
769
- let body = "";
770
- req.on("data", (chunk: Buffer) => { body += chunk.toString(); });
771
- req.on("end", () => {
772
- try {
773
- const msg = JSON.parse(body) as Record<string, unknown>;
774
- const agreementId = String(msg.agreementId ?? msg.agreement_id ?? "");
775
- const from = String(msg.from ?? "");
776
- log({ event: "delivery_accepted_inbound", agreementId, from });
777
- // Update DB: mark complete
778
- const all = db.listActiveHireRequests();
779
- const found = all.find(r => r.agreement_id === agreementId);
780
- if (found) db.updateHireRequestStatus(found.id, "complete");
781
- res.writeHead(200, { "Content-Type": "application/json" });
782
- res.end(JSON.stringify({ received: true, agent: config.wallet.contract_address }));
783
- } catch {
784
- res.writeHead(400, { "Content-Type": "application/json" });
785
- res.end(JSON.stringify({ error: "invalid_request" }));
786
- }
787
- });
917
+ const body = await readBody(req, res);
918
+ if (body === null) return;
919
+ try {
920
+ const msg = JSON.parse(body) as Record<string, unknown>;
921
+ const agreementId = String(msg.agreementId ?? msg.agreement_id ?? "");
922
+ const from = String(msg.from ?? "");
923
+ log({ event: "delivery_accepted_inbound", agreementId, from });
924
+ // Update DB: mark complete
925
+ const all = db.listActiveHireRequests();
926
+ const found = all.find(r => r.agreement_id === agreementId);
927
+ if (found) db.updateHireRequestStatus(found.id, "complete");
928
+ res.writeHead(200, { "Content-Type": "application/json" });
929
+ res.end(JSON.stringify({ received: true, agent: config.wallet.contract_address }));
930
+ } catch {
931
+ res.writeHead(400, { "Content-Type": "application/json" });
932
+ res.end(JSON.stringify({ error: "invalid_request" }));
933
+ }
788
934
  return;
789
935
  }
790
936
 
791
937
  // POST /dispute — dispute raised against this agent
792
938
  if (pathname === "/dispute" && req.method === "POST") {
793
- let body = "";
794
- req.on("data", (chunk: Buffer) => { body += chunk.toString(); });
795
- req.on("end", async () => {
796
- try {
797
- const msg = JSON.parse(body) as Record<string, unknown>;
798
- const agreementId = String(msg.agreementId ?? msg.agreement_id ?? "");
799
- const reason = String(msg.reason ?? "");
800
- const from = String(msg.from ?? "");
801
- log({ event: "dispute_received", agreementId, reason, from });
802
- if (config.notifications.notify_on_dispute) {
803
- await notifier.notifyDispute(agreementId, from);
804
- }
805
- res.writeHead(200, { "Content-Type": "application/json" });
806
- res.end(JSON.stringify({ received: true, agent: config.wallet.contract_address }));
807
- } catch {
808
- res.writeHead(400, { "Content-Type": "application/json" });
809
- res.end(JSON.stringify({ error: "invalid_request" }));
939
+ const body = await readBody(req, res);
940
+ if (body === null) return;
941
+ try {
942
+ const msg = JSON.parse(body) as Record<string, unknown>;
943
+ const agreementId = String(msg.agreementId ?? msg.agreement_id ?? "");
944
+ const reason = String(msg.reason ?? "");
945
+ const from = String(msg.from ?? "");
946
+ log({ event: "dispute_received", agreementId, reason, from });
947
+ if (config.notifications.notify_on_dispute) {
948
+ await notifier.notifyDispute(agreementId, from);
810
949
  }
811
- });
950
+ res.writeHead(200, { "Content-Type": "application/json" });
951
+ res.end(JSON.stringify({ received: true, agent: config.wallet.contract_address }));
952
+ } catch {
953
+ res.writeHead(400, { "Content-Type": "application/json" });
954
+ res.end(JSON.stringify({ error: "invalid_request" }));
955
+ }
812
956
  return;
813
957
  }
814
958
 
815
959
  // POST /dispute/resolved — dispute resolved by arbitrator
816
960
  if (pathname === "/dispute/resolved" && req.method === "POST") {
817
- let body = "";
818
- req.on("data", (chunk: Buffer) => { body += chunk.toString(); });
819
- req.on("end", () => {
820
- try {
821
- const msg = JSON.parse(body) as Record<string, unknown>;
822
- const agreementId = String(msg.agreementId ?? msg.agreement_id ?? "");
823
- const outcome = String(msg.outcome ?? "");
824
- const from = String(msg.from ?? "");
825
- log({ event: "dispute_resolved_inbound", agreementId, outcome, from });
826
- res.writeHead(200, { "Content-Type": "application/json" });
827
- res.end(JSON.stringify({ received: true, agent: config.wallet.contract_address }));
828
- } catch {
829
- res.writeHead(400, { "Content-Type": "application/json" });
830
- res.end(JSON.stringify({ error: "invalid_request" }));
831
- }
832
- });
961
+ const body = await readBody(req, res);
962
+ if (body === null) return;
963
+ try {
964
+ const msg = JSON.parse(body) as Record<string, unknown>;
965
+ const agreementId = String(msg.agreementId ?? msg.agreement_id ?? "");
966
+ const outcome = String(msg.outcome ?? "");
967
+ const from = String(msg.from ?? "");
968
+ log({ event: "dispute_resolved_inbound", agreementId, outcome, from });
969
+ res.writeHead(200, { "Content-Type": "application/json" });
970
+ res.end(JSON.stringify({ received: true, agent: config.wallet.contract_address }));
971
+ } catch {
972
+ res.writeHead(400, { "Content-Type": "application/json" });
973
+ res.end(JSON.stringify({ error: "invalid_request" }));
974
+ }
833
975
  return;
834
976
  }
835
977
 
836
978
  // POST /workroom/status — workroom lifecycle events
837
979
  if (pathname === "/workroom/status" && req.method === "POST") {
838
- let body = "";
839
- req.on("data", (chunk: Buffer) => { body += chunk.toString(); });
840
- req.on("end", () => {
841
- try {
842
- const msg = JSON.parse(body) as Record<string, unknown>;
843
- const event = String(msg.event ?? "");
844
- const agentAddress = String(msg.agentAddress ?? config.wallet.contract_address);
845
- const jobId = msg.jobId ? String(msg.jobId) : undefined;
846
- const timestamp = Number(msg.timestamp ?? Date.now());
847
- log({ event: "workroom_lifecycle", workroom_event: event, agentAddress, jobId, timestamp });
848
- res.writeHead(200, { "Content-Type": "application/json" });
849
- res.end(JSON.stringify({ received: true, agent: config.wallet.contract_address }));
850
- } catch {
851
- res.writeHead(400, { "Content-Type": "application/json" });
852
- res.end(JSON.stringify({ error: "invalid_request" }));
853
- }
854
- });
980
+ const body = await readBody(req, res);
981
+ if (body === null) return;
982
+ try {
983
+ const msg = JSON.parse(body) as Record<string, unknown>;
984
+ const event = String(msg.event ?? "");
985
+ const agentAddress = String(msg.agentAddress ?? config.wallet.contract_address);
986
+ const jobId = msg.jobId ? String(msg.jobId) : undefined;
987
+ const timestamp = Number(msg.timestamp ?? Date.now());
988
+ log({ event: "workroom_lifecycle", workroom_event: event, agentAddress, jobId, timestamp });
989
+ res.writeHead(200, { "Content-Type": "application/json" });
990
+ res.end(JSON.stringify({ received: true, agent: config.wallet.contract_address }));
991
+ } catch {
992
+ res.writeHead(400, { "Content-Type": "application/json" });
993
+ res.end(JSON.stringify({ error: "invalid_request" }));
994
+ }
855
995
  return;
856
996
  }
857
997
 
@@ -867,21 +1007,25 @@ export async function runDaemon(foreground = false): Promise<void> {
867
1007
  return;
868
1008
  }
869
1009
 
870
- // GET /status — health with active agreement count
1010
+ // GET /status — health with active agreement count (sensitive counts only for authenticated)
871
1011
  if (pathname === "/status" && req.method === "GET") {
872
- const activeList = db.listActiveHireRequests();
873
- const pendingList = db.listPendingHireRequests();
874
- res.writeHead(200, { "Content-Type": "application/json" });
875
- res.end(JSON.stringify({
1012
+ const statusAuth = (req.headers["authorization"] ?? "") as string;
1013
+ const statusToken = statusAuth.startsWith("Bearer ") ? statusAuth.slice(7) : "";
1014
+ const statusAuthed = statusToken === apiToken;
1015
+ const statusPayload: Record<string, unknown> = {
876
1016
  protocol: "arc-402",
877
1017
  version: "0.3.0",
878
1018
  agent: config.wallet.contract_address,
879
1019
  status: "online",
880
1020
  uptime: Math.floor((Date.now() - ipcCtx.startTime) / 1000),
881
- active_agreements: activeList.length,
882
- pending_approval: pendingList.length,
883
1021
  capabilities: config.policy.allowed_capabilities,
884
- }));
1022
+ };
1023
+ if (statusAuthed) {
1024
+ statusPayload.active_agreements = db.listActiveHireRequests().length;
1025
+ statusPayload.pending_approval = db.listPendingHireRequests().length;
1026
+ }
1027
+ res.writeHead(200, { "Content-Type": "application/json" });
1028
+ res.end(JSON.stringify(statusPayload));
885
1029
  return;
886
1030
  }
887
1031
 
@@ -917,6 +1061,7 @@ export async function runDaemon(foreground = false): Promise<void> {
917
1061
  if (relayInterval) clearInterval(relayInterval);
918
1062
  clearInterval(timeoutInterval);
919
1063
  clearInterval(balanceInterval);
1064
+ if (rateLimitCleanupInterval) clearInterval(rateLimitCleanupInterval);
920
1065
 
921
1066
  // Close HTTP + IPC
922
1067
  httpServer.close();