arc402-cli 0.6.0 → 0.7.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.
@@ -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,38 @@ 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
+ // ─── Body size limit ──────────────────────────────────────────────────────────
179
+ const MAX_BODY_SIZE = 1024 * 1024; // 1 MB
147
180
  // ─── Logger ───────────────────────────────────────────────────────────────────
148
181
  function openLogger(logPath, foreground) {
149
182
  let stream = null;
@@ -161,13 +194,14 @@ function openLogger(logPath, foreground) {
161
194
  }
162
195
  };
163
196
  }
164
- function startIpcServer(ctx, log) {
197
+ function startIpcServer(ctx, log, apiToken) {
165
198
  // Remove stale socket
166
199
  if (fs.existsSync(config_1.DAEMON_SOCK)) {
167
200
  fs.unlinkSync(config_1.DAEMON_SOCK);
168
201
  }
169
202
  const server = net.createServer((socket) => {
170
203
  let buf = "";
204
+ let authenticated = false;
171
205
  socket.on("data", (data) => {
172
206
  buf += data.toString();
173
207
  const lines = buf.split("\n");
@@ -183,6 +217,19 @@ function startIpcServer(ctx, log) {
183
217
  socket.write(JSON.stringify({ ok: false, error: "invalid_json" }) + "\n");
184
218
  continue;
185
219
  }
220
+ // First message must be auth
221
+ if (!authenticated) {
222
+ if (cmd.auth === apiToken) {
223
+ authenticated = true;
224
+ socket.write(JSON.stringify({ ok: true, authenticated: true }) + "\n");
225
+ }
226
+ else {
227
+ log({ event: "ipc_auth_failed" });
228
+ socket.write(JSON.stringify({ ok: false, error: "unauthorized" }) + "\n");
229
+ socket.destroy();
230
+ }
231
+ continue;
232
+ }
186
233
  const response = handleIpcCommand(cmd, ctx, log);
187
234
  socket.write(JSON.stringify(response) + "\n");
188
235
  }
@@ -481,10 +528,38 @@ async function runDaemon(foreground = false) {
481
528
  fs.writeFileSync(config_1.DAEMON_PID, String(process.pid), { mode: 0o600 });
482
529
  log({ event: "pid_written", pid: process.pid, path: config_1.DAEMON_PID });
483
530
  }
531
+ // ── Generate and save API token ──────────────────────────────────────────
532
+ const apiToken = generateApiToken();
533
+ saveApiToken(apiToken);
534
+ log({ event: "auth_token_saved", path: DAEMON_TOKEN_FILE });
484
535
  // ── Start IPC socket ─────────────────────────────────────────────────────
485
- const ipcServer = startIpcServer(ipcCtx, log);
536
+ const ipcServer = startIpcServer(ipcCtx, log, apiToken);
486
537
  // ── Start HTTP relay server (public endpoint) ────────────────────────────
487
538
  const httpPort = config.relay.listen_port ?? 4402;
539
+ /**
540
+ * Read request body with a size cap. Destroys the request and sends 413
541
+ * if the body exceeds MAX_BODY_SIZE. Returns null in that case.
542
+ */
543
+ function readBody(req, res) {
544
+ return new Promise((resolve) => {
545
+ let body = "";
546
+ let size = 0;
547
+ req.on("data", (chunk) => {
548
+ size += chunk.length;
549
+ if (size > MAX_BODY_SIZE) {
550
+ req.destroy();
551
+ res.writeHead(413, { "Content-Type": "application/json" });
552
+ res.end(JSON.stringify({ error: "payload_too_large" }));
553
+ resolve(null);
554
+ return;
555
+ }
556
+ body += chunk.toString();
557
+ });
558
+ req.on("end", () => { resolve(body); });
559
+ req.on("error", () => { resolve(null); });
560
+ });
561
+ }
562
+ const PUBLIC_GET_PATHS = new Set(["/", "/health", "/agent", "/capabilities", "/status"]);
488
563
  const httpServer = http.createServer(async (req, res) => {
489
564
  // CORS headers
490
565
  res.setHeader("Access-Control-Allow-Origin", "*");
@@ -497,6 +572,25 @@ async function runDaemon(foreground = false) {
497
572
  }
498
573
  const url = new URL(req.url || "/", `http://localhost:${httpPort}`);
499
574
  const pathname = url.pathname;
575
+ // Rate limiting (all endpoints)
576
+ const clientIp = (req.socket.remoteAddress ?? "unknown").replace(/^::ffff:/, "");
577
+ if (!checkRateLimit(clientIp)) {
578
+ log({ event: "rate_limited", ip: clientIp, path: pathname });
579
+ res.writeHead(429, { "Content-Type": "application/json" });
580
+ res.end(JSON.stringify({ error: "too_many_requests" }));
581
+ return;
582
+ }
583
+ // Auth required on all POST endpoints (GET public paths are open)
584
+ if (req.method === "POST" || (req.method === "GET" && !PUBLIC_GET_PATHS.has(pathname))) {
585
+ const authHeader = req.headers["authorization"] ?? "";
586
+ const token = authHeader.startsWith("Bearer ") ? authHeader.slice(7) : "";
587
+ if (token !== apiToken) {
588
+ log({ event: "http_unauthorized", ip: clientIp, path: pathname });
589
+ res.writeHead(401, { "Content-Type": "application/json" });
590
+ res.end(JSON.stringify({ error: "unauthorized" }));
591
+ return;
592
+ }
593
+ }
500
594
  // Health / info
501
595
  if (pathname === "/" || pathname === "/health") {
502
596
  const info = {
@@ -526,51 +620,35 @@ async function runDaemon(foreground = false) {
526
620
  }
527
621
  // Receive hire proposal
528
622
  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";
623
+ const body = await readBody(req, res);
624
+ if (body === null)
625
+ return;
626
+ try {
627
+ const msg = JSON.parse(body);
628
+ // Feed into the hire listener's message handler
629
+ const proposal = {
630
+ messageId: String(msg.messageId ?? msg.id ?? `http_${Date.now()}`),
631
+ hirerAddress: String(msg.hirerAddress ?? msg.hirer_address ?? msg.from ?? ""),
632
+ capability: String(msg.capability ?? ""),
633
+ priceEth: String(msg.priceEth ?? msg.price_eth ?? "0"),
634
+ deadlineUnix: Number(msg.deadlineUnix ?? msg.deadline ?? 0),
635
+ specHash: String(msg.specHash ?? msg.spec_hash ?? ""),
636
+ agreementId: msg.agreementId ? String(msg.agreementId) : undefined,
637
+ signature: msg.signature ? String(msg.signature) : undefined,
638
+ };
639
+ // Dedup
640
+ const existing = db.getHireRequest(proposal.messageId);
641
+ if (existing) {
642
+ log({ event: "hire_duplicate", id: proposal.messageId, status: existing.status });
643
+ res.writeHead(200, { "Content-Type": "application/json" });
644
+ res.end(JSON.stringify({ status: existing.status, id: proposal.messageId }));
645
+ return;
646
+ }
647
+ // Policy check
648
+ const { evaluatePolicy } = await Promise.resolve().then(() => __importStar(require("./hire-listener")));
649
+ const activeCount = db.countActiveHireRequests();
650
+ const policyResult = evaluatePolicy(proposal, config, activeCount);
651
+ if (!policyResult.allowed) {
574
652
  db.insertHireRequest({
575
653
  id: proposal.messageId,
576
654
  agreement_id: proposal.agreementId ?? null,
@@ -579,206 +657,214 @@ async function runDaemon(foreground = false) {
579
657
  price_eth: proposal.priceEth,
580
658
  deadline_unix: proposal.deadlineUnix,
581
659
  spec_hash: proposal.specHash,
582
- status,
583
- reject_reason: null,
660
+ status: "rejected",
661
+ reject_reason: policyResult.reason ?? "policy_violation",
584
662
  });
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
- }
663
+ log({ event: "http_hire_rejected", id: proposal.messageId, reason: policyResult.reason });
589
664
  res.writeHead(200, { "Content-Type": "application/json" });
590
- res.end(JSON.stringify({ status, id: proposal.messageId }));
665
+ res.end(JSON.stringify({ status: "rejected", reason: policyResult.reason, id: proposal.messageId }));
666
+ return;
591
667
  }
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" }));
668
+ const status = config.policy.auto_accept ? "accepted" : "pending_approval";
669
+ db.insertHireRequest({
670
+ id: proposal.messageId,
671
+ agreement_id: proposal.agreementId ?? null,
672
+ hirer_address: proposal.hirerAddress,
673
+ capability: proposal.capability,
674
+ price_eth: proposal.priceEth,
675
+ deadline_unix: proposal.deadlineUnix,
676
+ spec_hash: proposal.specHash,
677
+ status,
678
+ reject_reason: null,
679
+ });
680
+ log({ event: "http_hire_received", id: proposal.messageId, hirer: proposal.hirerAddress, status });
681
+ if (config.notifications.notify_on_hire_request) {
682
+ await notifier.notifyHireRequest(proposal.messageId, proposal.hirerAddress, proposal.priceEth, proposal.capability);
596
683
  }
597
- });
684
+ res.writeHead(200, { "Content-Type": "application/json" });
685
+ res.end(JSON.stringify({ status, id: proposal.messageId }));
686
+ }
687
+ catch (err) {
688
+ log({ event: "http_hire_error", error: String(err) });
689
+ res.writeHead(400, { "Content-Type": "application/json" });
690
+ res.end(JSON.stringify({ error: "invalid_request" }));
691
+ }
598
692
  return;
599
693
  }
600
694
  // Handshake acknowledgment endpoint
601
695
  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
- });
696
+ const body = await readBody(req, res);
697
+ if (body === null)
698
+ return;
699
+ try {
700
+ const msg = JSON.parse(body);
701
+ log({ event: "handshake_received", from: msg.from, type: msg.type, note: msg.note });
702
+ res.writeHead(200, { "Content-Type": "application/json" });
703
+ res.end(JSON.stringify({ received: true, agent: config.wallet.contract_address }));
704
+ }
705
+ catch {
706
+ res.writeHead(400, { "Content-Type": "application/json" });
707
+ res.end(JSON.stringify({ error: "invalid_request" }));
708
+ }
616
709
  return;
617
710
  }
618
711
  // POST /hire/accepted — provider accepted, client notified
619
712
  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 }));
713
+ const body = await readBody(req, res);
714
+ if (body === null)
715
+ return;
716
+ try {
717
+ const msg = JSON.parse(body);
718
+ const agreementId = String(msg.agreementId ?? msg.agreement_id ?? "");
719
+ const from = String(msg.from ?? "");
720
+ log({ event: "hire_accepted_inbound", agreementId, from });
721
+ if (config.notifications.notify_on_hire_accepted) {
722
+ await notifier.notifyHireAccepted(agreementId, agreementId);
633
723
  }
634
- catch {
635
- res.writeHead(400, { "Content-Type": "application/json" });
636
- res.end(JSON.stringify({ error: "invalid_request" }));
637
- }
638
- });
724
+ res.writeHead(200, { "Content-Type": "application/json" });
725
+ res.end(JSON.stringify({ received: true, agent: config.wallet.contract_address }));
726
+ }
727
+ catch {
728
+ res.writeHead(400, { "Content-Type": "application/json" });
729
+ res.end(JSON.stringify({ error: "invalid_request" }));
730
+ }
639
731
  return;
640
732
  }
641
733
  // POST /message — off-chain negotiation message
642
734
  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
- });
735
+ const body = await readBody(req, res);
736
+ if (body === null)
737
+ return;
738
+ try {
739
+ const msg = JSON.parse(body);
740
+ const from = String(msg.from ?? "");
741
+ const to = String(msg.to ?? "");
742
+ const content = String(msg.content ?? "");
743
+ const timestamp = Number(msg.timestamp ?? Date.now());
744
+ log({ event: "message_received", from, to, timestamp, content_len: content.length });
745
+ res.writeHead(200, { "Content-Type": "application/json" });
746
+ res.end(JSON.stringify({ received: true, agent: config.wallet.contract_address }));
747
+ }
748
+ catch {
749
+ res.writeHead(400, { "Content-Type": "application/json" });
750
+ res.end(JSON.stringify({ error: "invalid_request" }));
751
+ }
661
752
  return;
662
753
  }
663
754
  // POST /delivery — provider committed a deliverable
664
755
  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" }));
756
+ const body = await readBody(req, res);
757
+ if (body === null)
758
+ return;
759
+ try {
760
+ const msg = JSON.parse(body);
761
+ const agreementId = String(msg.agreementId ?? msg.agreement_id ?? "");
762
+ const deliverableHash = String(msg.deliverableHash ?? msg.deliverable_hash ?? "");
763
+ const from = String(msg.from ?? "");
764
+ log({ event: "delivery_received", agreementId, deliverableHash, from });
765
+ // Update DB: mark delivered
766
+ const active = db.listActiveHireRequests();
767
+ const found = active.find(r => r.agreement_id === agreementId);
768
+ if (found)
769
+ db.updateHireRequestStatus(found.id, "delivered");
770
+ if (config.notifications.notify_on_delivery) {
771
+ await notifier.notifyDelivery(agreementId, deliverableHash, "");
688
772
  }
689
- });
773
+ res.writeHead(200, { "Content-Type": "application/json" });
774
+ res.end(JSON.stringify({ received: true, agent: config.wallet.contract_address }));
775
+ }
776
+ catch {
777
+ res.writeHead(400, { "Content-Type": "application/json" });
778
+ res.end(JSON.stringify({ error: "invalid_request" }));
779
+ }
690
780
  return;
691
781
  }
692
782
  // POST /delivery/accepted — client accepted delivery, payment releasing
693
783
  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
- });
784
+ const body = await readBody(req, res);
785
+ if (body === null)
786
+ return;
787
+ try {
788
+ const msg = JSON.parse(body);
789
+ const agreementId = String(msg.agreementId ?? msg.agreement_id ?? "");
790
+ const from = String(msg.from ?? "");
791
+ log({ event: "delivery_accepted_inbound", agreementId, from });
792
+ // Update DB: mark complete
793
+ const all = db.listActiveHireRequests();
794
+ const found = all.find(r => r.agreement_id === agreementId);
795
+ if (found)
796
+ db.updateHireRequestStatus(found.id, "complete");
797
+ res.writeHead(200, { "Content-Type": "application/json" });
798
+ res.end(JSON.stringify({ received: true, agent: config.wallet.contract_address }));
799
+ }
800
+ catch {
801
+ res.writeHead(400, { "Content-Type": "application/json" });
802
+ res.end(JSON.stringify({ error: "invalid_request" }));
803
+ }
715
804
  return;
716
805
  }
717
806
  // POST /dispute — dispute raised against this agent
718
807
  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 }));
808
+ const body = await readBody(req, res);
809
+ if (body === null)
810
+ return;
811
+ try {
812
+ const msg = JSON.parse(body);
813
+ const agreementId = String(msg.agreementId ?? msg.agreement_id ?? "");
814
+ const reason = String(msg.reason ?? "");
815
+ const from = String(msg.from ?? "");
816
+ log({ event: "dispute_received", agreementId, reason, from });
817
+ if (config.notifications.notify_on_dispute) {
818
+ await notifier.notifyDispute(agreementId, from);
733
819
  }
734
- catch {
735
- res.writeHead(400, { "Content-Type": "application/json" });
736
- res.end(JSON.stringify({ error: "invalid_request" }));
737
- }
738
- });
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
+ }
739
827
  return;
740
828
  }
741
829
  // POST /dispute/resolved — dispute resolved by arbitrator
742
830
  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
- });
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 outcome = String(msg.outcome ?? "");
838
+ const from = String(msg.from ?? "");
839
+ log({ event: "dispute_resolved_inbound", agreementId, outcome, from });
840
+ res.writeHead(200, { "Content-Type": "application/json" });
841
+ res.end(JSON.stringify({ received: true, agent: config.wallet.contract_address }));
842
+ }
843
+ catch {
844
+ res.writeHead(400, { "Content-Type": "application/json" });
845
+ res.end(JSON.stringify({ error: "invalid_request" }));
846
+ }
760
847
  return;
761
848
  }
762
849
  // POST /workroom/status — workroom lifecycle events
763
850
  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
- });
851
+ const body = await readBody(req, res);
852
+ if (body === null)
853
+ return;
854
+ try {
855
+ const msg = JSON.parse(body);
856
+ const event = String(msg.event ?? "");
857
+ const agentAddress = String(msg.agentAddress ?? config.wallet.contract_address);
858
+ const jobId = msg.jobId ? String(msg.jobId) : undefined;
859
+ const timestamp = Number(msg.timestamp ?? Date.now());
860
+ log({ event: "workroom_lifecycle", workroom_event: event, agentAddress, jobId, timestamp });
861
+ res.writeHead(200, { "Content-Type": "application/json" });
862
+ res.end(JSON.stringify({ received: true, agent: config.wallet.contract_address }));
863
+ }
864
+ catch {
865
+ res.writeHead(400, { "Content-Type": "application/json" });
866
+ res.end(JSON.stringify({ error: "invalid_request" }));
867
+ }
782
868
  return;
783
869
  }
784
870
  // GET /capabilities — agent capabilities from config