arc402-cli 0.7.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.
- package/dist/commands/backup.d.ts +3 -0
- package/dist/commands/backup.d.ts.map +1 -0
- package/dist/commands/backup.js +106 -0
- package/dist/commands/backup.js.map +1 -0
- package/dist/commands/discover.d.ts.map +1 -1
- package/dist/commands/discover.js +60 -15
- package/dist/commands/discover.js.map +1 -1
- package/dist/commands/wallet.d.ts.map +1 -1
- package/dist/commands/wallet.js +136 -52
- package/dist/commands/wallet.js.map +1 -1
- package/dist/commands/watch.d.ts.map +1 -1
- package/dist/commands/watch.js +146 -9
- package/dist/commands/watch.js.map +1 -1
- package/dist/config.d.ts +8 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +25 -1
- package/dist/config.js.map +1 -1
- package/dist/daemon/index.d.ts.map +1 -1
- package/dist/daemon/index.js +65 -12
- package/dist/daemon/index.js.map +1 -1
- package/dist/endpoint-notify.d.ts +2 -1
- package/dist/endpoint-notify.d.ts.map +1 -1
- package/dist/endpoint-notify.js +12 -3
- package/dist/endpoint-notify.js.map +1 -1
- package/dist/index.js +26 -0
- package/dist/index.js.map +1 -1
- package/dist/program.d.ts.map +1 -1
- package/dist/program.js +2 -0
- package/dist/program.js.map +1 -1
- package/package.json +1 -1
- package/src/commands/backup.ts +117 -0
- package/src/commands/discover.ts +74 -21
- package/src/commands/wallet.ts +137 -51
- package/src/commands/watch.ts +207 -10
- package/src/config.ts +39 -1
- package/src/daemon/index.ts +63 -12
- package/src/endpoint-notify.ts +13 -3
- package/src/index.ts +26 -0
- package/src/program.ts +2 -0
package/src/daemon/index.ts
CHANGED
|
@@ -188,6 +188,9 @@ function checkRateLimit(ip: string): boolean {
|
|
|
188
188
|
return bucket.count <= RATE_LIMIT;
|
|
189
189
|
}
|
|
190
190
|
|
|
191
|
+
// Cleanup stale rate limit entries every 5 minutes to prevent unbounded growth
|
|
192
|
+
let rateLimitCleanupInterval: ReturnType<typeof setInterval> | null = null;
|
|
193
|
+
|
|
191
194
|
// ─── Body size limit ──────────────────────────────────────────────────────────
|
|
192
195
|
|
|
193
196
|
const MAX_BODY_SIZE = 1024 * 1024; // 1 MB
|
|
@@ -588,6 +591,14 @@ export async function runDaemon(foreground = false): Promise<void> {
|
|
|
588
591
|
}
|
|
589
592
|
}, 30_000);
|
|
590
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
|
+
|
|
591
602
|
// Balance monitor — every 5 minutes
|
|
592
603
|
const balanceInterval = setInterval(async () => {
|
|
593
604
|
try {
|
|
@@ -617,6 +628,26 @@ export async function runDaemon(foreground = false): Promise<void> {
|
|
|
617
628
|
// ── Start HTTP relay server (public endpoint) ────────────────────────────
|
|
618
629
|
const httpPort = config.relay.listen_port ?? 4402;
|
|
619
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
|
+
|
|
620
651
|
/**
|
|
621
652
|
* Read request body with a size cap. Destroys the request and sends 413
|
|
622
653
|
* if the body exceeds MAX_BODY_SIZE. Returns null in that case.
|
|
@@ -643,11 +674,22 @@ export async function runDaemon(foreground = false): Promise<void> {
|
|
|
643
674
|
|
|
644
675
|
const PUBLIC_GET_PATHS = new Set(["/", "/health", "/agent", "/capabilities", "/status"]);
|
|
645
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
|
+
|
|
646
680
|
const httpServer = http.createServer(async (req, res) => {
|
|
647
|
-
// CORS
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
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
|
+
}
|
|
651
693
|
if (req.method === "OPTIONS") { res.writeHead(204); res.end(); return; }
|
|
652
694
|
|
|
653
695
|
const url = new URL(req.url || "/", `http://localhost:${httpPort}`);
|
|
@@ -707,6 +749,7 @@ export async function runDaemon(foreground = false): Promise<void> {
|
|
|
707
749
|
if (pathname === "/hire" && req.method === "POST") {
|
|
708
750
|
const body = await readBody(req, res);
|
|
709
751
|
if (body === null) return;
|
|
752
|
+
verifyRequestSignature(body, req);
|
|
710
753
|
try {
|
|
711
754
|
const msg = JSON.parse(body) as Record<string, unknown>;
|
|
712
755
|
|
|
@@ -787,6 +830,7 @@ export async function runDaemon(foreground = false): Promise<void> {
|
|
|
787
830
|
if (pathname === "/handshake" && req.method === "POST") {
|
|
788
831
|
const body = await readBody(req, res);
|
|
789
832
|
if (body === null) return;
|
|
833
|
+
verifyRequestSignature(body, req);
|
|
790
834
|
try {
|
|
791
835
|
const msg = JSON.parse(body);
|
|
792
836
|
log({ event: "handshake_received", from: msg.from, type: msg.type, note: msg.note });
|
|
@@ -824,6 +868,7 @@ export async function runDaemon(foreground = false): Promise<void> {
|
|
|
824
868
|
if (pathname === "/message" && req.method === "POST") {
|
|
825
869
|
const body = await readBody(req, res);
|
|
826
870
|
if (body === null) return;
|
|
871
|
+
verifyRequestSignature(body, req);
|
|
827
872
|
try {
|
|
828
873
|
const msg = JSON.parse(body) as Record<string, unknown>;
|
|
829
874
|
const from = String(msg.from ?? "");
|
|
@@ -844,6 +889,7 @@ export async function runDaemon(foreground = false): Promise<void> {
|
|
|
844
889
|
if (pathname === "/delivery" && req.method === "POST") {
|
|
845
890
|
const body = await readBody(req, res);
|
|
846
891
|
if (body === null) return;
|
|
892
|
+
verifyRequestSignature(body, req);
|
|
847
893
|
try {
|
|
848
894
|
const msg = JSON.parse(body) as Record<string, unknown>;
|
|
849
895
|
const agreementId = String(msg.agreementId ?? msg.agreement_id ?? "");
|
|
@@ -961,21 +1007,25 @@ export async function runDaemon(foreground = false): Promise<void> {
|
|
|
961
1007
|
return;
|
|
962
1008
|
}
|
|
963
1009
|
|
|
964
|
-
// GET /status — health with active agreement count
|
|
1010
|
+
// GET /status — health with active agreement count (sensitive counts only for authenticated)
|
|
965
1011
|
if (pathname === "/status" && req.method === "GET") {
|
|
966
|
-
const
|
|
967
|
-
const
|
|
968
|
-
|
|
969
|
-
|
|
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> = {
|
|
970
1016
|
protocol: "arc-402",
|
|
971
1017
|
version: "0.3.0",
|
|
972
1018
|
agent: config.wallet.contract_address,
|
|
973
1019
|
status: "online",
|
|
974
1020
|
uptime: Math.floor((Date.now() - ipcCtx.startTime) / 1000),
|
|
975
|
-
active_agreements: activeList.length,
|
|
976
|
-
pending_approval: pendingList.length,
|
|
977
1021
|
capabilities: config.policy.allowed_capabilities,
|
|
978
|
-
}
|
|
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));
|
|
979
1029
|
return;
|
|
980
1030
|
}
|
|
981
1031
|
|
|
@@ -1011,6 +1061,7 @@ export async function runDaemon(foreground = false): Promise<void> {
|
|
|
1011
1061
|
if (relayInterval) clearInterval(relayInterval);
|
|
1012
1062
|
clearInterval(timeoutInterval);
|
|
1013
1063
|
clearInterval(balanceInterval);
|
|
1064
|
+
if (rateLimitCleanupInterval) clearInterval(rateLimitCleanupInterval);
|
|
1014
1065
|
|
|
1015
1066
|
// Close HTTP + IPC
|
|
1016
1067
|
httpServer.close();
|
package/src/endpoint-notify.ts
CHANGED
|
@@ -92,11 +92,13 @@ export async function resolveAgentEndpoint(
|
|
|
92
92
|
* POSTs JSON payload to {endpoint}{path}. Returns true on success.
|
|
93
93
|
* Never throws — logs a warning on failure.
|
|
94
94
|
* Validates endpoint URL for SSRF before connecting.
|
|
95
|
+
* If signingKey is provided, signs the payload and adds X-ARC402-Signature / X-ARC402-Signer headers.
|
|
95
96
|
*/
|
|
96
97
|
export async function notifyAgent(
|
|
97
98
|
endpoint: string,
|
|
98
99
|
path: string,
|
|
99
|
-
payload: Record<string, unknown
|
|
100
|
+
payload: Record<string, unknown>,
|
|
101
|
+
signingKey?: string
|
|
100
102
|
): Promise<boolean> {
|
|
101
103
|
if (!endpoint) return false;
|
|
102
104
|
try {
|
|
@@ -106,10 +108,18 @@ export async function notifyAgent(
|
|
|
106
108
|
return false;
|
|
107
109
|
}
|
|
108
110
|
try {
|
|
111
|
+
const body = JSON.stringify(payload);
|
|
112
|
+
const headers: Record<string, string> = { "Content-Type": "application/json" };
|
|
113
|
+
if (signingKey) {
|
|
114
|
+
const wallet = new ethers.Wallet(signingKey);
|
|
115
|
+
const signature = await wallet.signMessage(body);
|
|
116
|
+
headers["X-ARC402-Signature"] = signature;
|
|
117
|
+
headers["X-ARC402-Signer"] = wallet.address;
|
|
118
|
+
}
|
|
109
119
|
const res = await fetch(`${endpoint}${path}`, {
|
|
110
120
|
method: "POST",
|
|
111
|
-
headers
|
|
112
|
-
body
|
|
121
|
+
headers,
|
|
122
|
+
body,
|
|
113
123
|
});
|
|
114
124
|
return res.ok;
|
|
115
125
|
} catch (err) {
|
package/src/index.ts
CHANGED
|
@@ -1,6 +1,29 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { createProgram } from "./program";
|
|
3
3
|
import { startREPL } from "./repl";
|
|
4
|
+
import { configExists, loadConfig, saveConfig } from "./config";
|
|
5
|
+
|
|
6
|
+
// ── Upgrade safety check ────────────────────────────────────────────────────
|
|
7
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
8
|
+
const currentVersion: string = (require("../package.json") as { version: string }).version;
|
|
9
|
+
|
|
10
|
+
function checkUpgrade(): void {
|
|
11
|
+
if (!configExists()) return;
|
|
12
|
+
try {
|
|
13
|
+
const config = loadConfig();
|
|
14
|
+
const prev = config.lastCliVersion;
|
|
15
|
+
if (prev && prev !== currentVersion) {
|
|
16
|
+
// Compare semver loosely — just print if different
|
|
17
|
+
console.log(`◈ Upgraded from ${prev} → ${currentVersion}`);
|
|
18
|
+
}
|
|
19
|
+
if (config.lastCliVersion !== currentVersion) {
|
|
20
|
+
// Never overwrite existing fields — only update lastCliVersion
|
|
21
|
+
saveConfig({ ...config, lastCliVersion: currentVersion });
|
|
22
|
+
}
|
|
23
|
+
} catch {
|
|
24
|
+
// Never crash on upgrade check
|
|
25
|
+
}
|
|
26
|
+
}
|
|
4
27
|
|
|
5
28
|
const printMode = process.argv.includes("--print");
|
|
6
29
|
|
|
@@ -11,6 +34,7 @@ if (printMode) {
|
|
|
11
34
|
process.env["NO_COLOR"] = "1";
|
|
12
35
|
process.env["FORCE_COLOR"] = "0";
|
|
13
36
|
process.env["ARC402_PRINT"] = "1";
|
|
37
|
+
checkUpgrade();
|
|
14
38
|
const program = createProgram();
|
|
15
39
|
void program.parseAsync(process.argv).then(() => process.exit(0)).catch((e: unknown) => {
|
|
16
40
|
console.error(e instanceof Error ? e.message : String(e));
|
|
@@ -18,9 +42,11 @@ if (printMode) {
|
|
|
18
42
|
});
|
|
19
43
|
} else if (process.argv.length <= 2) {
|
|
20
44
|
// No subcommand — enter interactive REPL
|
|
45
|
+
checkUpgrade();
|
|
21
46
|
void startREPL();
|
|
22
47
|
} else {
|
|
23
48
|
// One-shot mode — arc402 wallet deploy still works as usual
|
|
49
|
+
checkUpgrade();
|
|
24
50
|
const program = createProgram();
|
|
25
51
|
program.parse(process.argv);
|
|
26
52
|
}
|
package/src/program.ts
CHANGED
|
@@ -32,6 +32,7 @@ import { registerMigrateCommands } from "./commands/migrate";
|
|
|
32
32
|
import { registerFeedCommand } from "./commands/feed";
|
|
33
33
|
import { registerArenaCommands } from "./commands/arena";
|
|
34
34
|
import { registerWatchCommand } from "./commands/watch";
|
|
35
|
+
import { registerBackupCommand } from "./commands/backup";
|
|
35
36
|
import reputation from "./commands/reputation.js";
|
|
36
37
|
import policy from "./commands/policy.js";
|
|
37
38
|
|
|
@@ -78,6 +79,7 @@ export function createProgram(): Command {
|
|
|
78
79
|
registerFeedCommand(program);
|
|
79
80
|
registerArenaCommands(program);
|
|
80
81
|
registerWatchCommand(program);
|
|
82
|
+
registerBackupCommand(program);
|
|
81
83
|
program.addCommand(reputation);
|
|
82
84
|
program.addCommand(policy);
|
|
83
85
|
|