ape-claw 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (114) hide show
  1. package/.cursor/skills/ape-claw/SKILL.md +322 -0
  2. package/LICENSE +21 -0
  3. package/README.md +826 -0
  4. package/allowlists/opensea-slug-overrides.json +13 -0
  5. package/allowlists/recommended.apechain.json +322 -0
  6. package/config/clawbots.example.json +3 -0
  7. package/config/policy.example.json +27 -0
  8. package/data/starter-pack-bundle.json +1 -0
  9. package/data/starter-pack.json +495 -0
  10. package/docs/ACP_BOUNTIES.md +108 -0
  11. package/docs/APECLAW_V2_ALPHA.md +206 -0
  12. package/docs/AUTONOMY_AND_SUBSTRATE.md +69 -0
  13. package/docs/CLAWBOTS_AND_INVITES.md +102 -0
  14. package/docs/CLI_GUIDE.md +124 -0
  15. package/docs/CONTRIBUTING.md +130 -0
  16. package/docs/DASHBOARD_GUIDE.md +108 -0
  17. package/docs/GLOBAL_BACKEND.md +145 -0
  18. package/docs/ONCHAIN_V2_GUIDE.md +140 -0
  19. package/docs/PRODUCT_OVERVIEW.md +127 -0
  20. package/docs/README.md +40 -0
  21. package/docs/SKILLCARDS_AND_IMPORTER.md +147 -0
  22. package/docs/STARTER_PACK.md +297 -0
  23. package/docs/SUPPORTED_NETWORKS.md +58 -0
  24. package/docs/TELEMETRY_AND_EVENTS.md +103 -0
  25. package/docs/THE_POD_RUNNER.md +198 -0
  26. package/docs/V1_WORKFLOWS.md +108 -0
  27. package/docs/V2_ONCHAIN_SKILLS.md +157 -0
  28. package/docs/WEB4_PLAN_STATUS.md +95 -0
  29. package/docs/WEB4_SWARM_MODEL.md +104 -0
  30. package/docs/archive/AUTONOMY_AND_SUBSTRATE.md +66 -0
  31. package/docs/archive/WEB4_PLAN_STATUS.md +93 -0
  32. package/docs/archive/WEB4_SWARM_MODEL.md +98 -0
  33. package/docs/developer/01-architecture.md +345 -0
  34. package/docs/developer/02-contracts.md +1034 -0
  35. package/docs/developer/03-writing-modules.md +513 -0
  36. package/docs/developer/04-skillcard-spec.md +336 -0
  37. package/docs/developer/05-backend-api.md +1079 -0
  38. package/docs/developer/06-telemetry.md +798 -0
  39. package/docs/developer/07-testing.md +546 -0
  40. package/docs/developer/08-contributing.md +211 -0
  41. package/docs/operator/01-quickstart.md +49 -0
  42. package/docs/operator/02-dashboard.md +174 -0
  43. package/docs/operator/03-cli-reference.md +818 -0
  44. package/docs/operator/04-skills-library.md +169 -0
  45. package/docs/operator/05-pod-operations.md +314 -0
  46. package/docs/operator/06-deployment.md +299 -0
  47. package/docs/operator/07-safety-and-policy.md +311 -0
  48. package/docs/operator/08-troubleshooting.md +457 -0
  49. package/docs/operator/09-env-reference.md +238 -0
  50. package/docs/social/STARTER_PACK_THREAD.md +209 -0
  51. package/package.json +77 -0
  52. package/skillcards/import-sources.json +93 -0
  53. package/skillcards/seed/acp-bounty-poll.v1.json +38 -0
  54. package/skillcards/seed/acp-bounty-post.v1.json +55 -0
  55. package/skillcards/seed/acp-browse.v1.json +41 -0
  56. package/skillcards/seed/acp-fulfill-and-route.v1.json +56 -0
  57. package/skillcards/seed/apeclaw-bridge-relay.v1.json +46 -0
  58. package/skillcards/seed/apeclaw-nft-autobuy.v1.json +60 -0
  59. package/skillcards/seed/apeclaw-receipt-recorder.v1.json +64 -0
  60. package/skillcards/seed/humanizer.v1.json +74 -0
  61. package/skillcards/seed/otherside-navigator.v1.json +116 -0
  62. package/skillcards/seed/stonkbrokers-launcher.v1.json +280 -0
  63. package/skillcards/seed/walkie-p2p.v1.json +66 -0
  64. package/src/cli/index.mjs +8 -0
  65. package/src/cli.mjs +1929 -0
  66. package/src/lib/bridge-relay.mjs +294 -0
  67. package/src/lib/clawbots.mjs +94 -0
  68. package/src/lib/io.mjs +36 -0
  69. package/src/lib/market.mjs +233 -0
  70. package/src/lib/nft-opensea.mjs +159 -0
  71. package/src/lib/paths.mjs +17 -0
  72. package/src/lib/pod-init.mjs +40 -0
  73. package/src/lib/policy.mjs +112 -0
  74. package/src/lib/rpc.mjs +49 -0
  75. package/src/lib/telemetry.mjs +92 -0
  76. package/src/lib/v2-onchain-abi.mjs +294 -0
  77. package/src/lib/v2-skillcard.mjs +27 -0
  78. package/src/server/index.mjs +169 -0
  79. package/src/server/logger.mjs +21 -0
  80. package/src/server/middleware/auth.mjs +90 -0
  81. package/src/server/middleware/body-limit.mjs +35 -0
  82. package/src/server/middleware/cors.mjs +33 -0
  83. package/src/server/middleware/rate-limit.mjs +44 -0
  84. package/src/server/routes/chat.mjs +178 -0
  85. package/src/server/routes/clawbots.mjs +182 -0
  86. package/src/server/routes/events.mjs +95 -0
  87. package/src/server/routes/health.mjs +72 -0
  88. package/src/server/routes/pod.mjs +64 -0
  89. package/src/server/routes/quotes.mjs +161 -0
  90. package/src/server/routes/skills.mjs +239 -0
  91. package/src/server/routes/static.mjs +161 -0
  92. package/src/server/routes/v2.mjs +48 -0
  93. package/src/server/sse.mjs +73 -0
  94. package/src/server/storage/file-backend.mjs +295 -0
  95. package/src/server/storage/index.mjs +37 -0
  96. package/src/server/storage/sqlite-backend.mjs +380 -0
  97. package/src/telemetry-server.mjs +1604 -0
  98. package/ui/css/dashboard.css +792 -0
  99. package/ui/css/skills.css +689 -0
  100. package/ui/docs.html +840 -0
  101. package/ui/favicon-180.png +0 -0
  102. package/ui/favicon-192.png +0 -0
  103. package/ui/favicon-32.png +0 -0
  104. package/ui/favicon-lobster.png +0 -0
  105. package/ui/favicon.svg +10 -0
  106. package/ui/index.html +2957 -0
  107. package/ui/js/dashboard.js +1766 -0
  108. package/ui/js/skills.js +1621 -0
  109. package/ui/pod.html +909 -0
  110. package/ui/shared/motion.css +286 -0
  111. package/ui/shared/motion.js +170 -0
  112. package/ui/shared/sidebar-nav.css +379 -0
  113. package/ui/shared/sidebar-nav.js +137 -0
  114. package/ui/skills.html +2879 -0
@@ -0,0 +1,294 @@
1
+ export const SkillNFT_ABI = [
2
+ {
3
+ type: "function",
4
+ name: "mintSkill",
5
+ stateMutability: "nonpayable",
6
+ inputs: [{ name: "parentId", type: "uint256" }],
7
+ outputs: [{ name: "skillId", type: "uint256" }],
8
+ },
9
+ {
10
+ type: "function",
11
+ name: "mintSkillWithRoyalty",
12
+ stateMutability: "nonpayable",
13
+ inputs: [
14
+ { name: "parentId", type: "uint256" },
15
+ { name: "receiver", type: "address" },
16
+ { name: "feeBps", type: "uint96" },
17
+ ],
18
+ outputs: [{ name: "skillId", type: "uint256" }],
19
+ },
20
+ {
21
+ type: "function",
22
+ name: "setSkillRoyalty",
23
+ stateMutability: "nonpayable",
24
+ inputs: [
25
+ { name: "skillId", type: "uint256" },
26
+ { name: "receiver", type: "address" },
27
+ { name: "feeBps", type: "uint96" },
28
+ ],
29
+ outputs: [],
30
+ },
31
+ {
32
+ type: "function",
33
+ name: "royaltyInfo",
34
+ stateMutability: "view",
35
+ inputs: [
36
+ { name: "tokenId", type: "uint256" },
37
+ { name: "salePrice", type: "uint256" },
38
+ ],
39
+ outputs: [
40
+ { name: "receiver", type: "address" },
41
+ { name: "royaltyAmount", type: "uint256" },
42
+ ],
43
+ },
44
+ {
45
+ type: "function",
46
+ name: "nextSkillId",
47
+ stateMutability: "view",
48
+ inputs: [],
49
+ outputs: [{ type: "uint256" }],
50
+ },
51
+ ];
52
+
53
+ export const SkillRegistry_ABI = [
54
+ {
55
+ type: "function",
56
+ name: "publishVersion",
57
+ stateMutability: "nonpayable",
58
+ inputs: [
59
+ { name: "skillId", type: "uint256" },
60
+ { name: "versionHash", type: "bytes32" },
61
+ { name: "contentHash", type: "bytes32" },
62
+ { name: "uri", type: "string" },
63
+ { name: "riskTier", type: "uint8" },
64
+ ],
65
+ outputs: [],
66
+ },
67
+ {
68
+ type: "function",
69
+ name: "versionCount",
70
+ stateMutability: "view",
71
+ inputs: [{ name: "skillId", type: "uint256" }],
72
+ outputs: [{ type: "uint256" }],
73
+ },
74
+ ];
75
+
76
+ export const IntentRegistry_ABI = [
77
+ {
78
+ type: "function",
79
+ name: "createIntent",
80
+ stateMutability: "nonpayable",
81
+ inputs: [
82
+ { name: "intentHash", type: "bytes32" },
83
+ { name: "expiresAt", type: "uint64" },
84
+ ],
85
+ outputs: [{ name: "intentId", type: "uint256" }],
86
+ },
87
+ {
88
+ type: "function",
89
+ name: "cancelIntent",
90
+ stateMutability: "nonpayable",
91
+ inputs: [{ name: "intentId", type: "uint256" }],
92
+ outputs: [],
93
+ },
94
+ {
95
+ type: "function",
96
+ name: "isActive",
97
+ stateMutability: "view",
98
+ inputs: [{ name: "intentId", type: "uint256" }],
99
+ outputs: [{ type: "bool" }],
100
+ },
101
+ ];
102
+
103
+ export const ReceiptRegistry_ABI = [
104
+ {
105
+ type: "function",
106
+ name: "recordReceipt",
107
+ stateMutability: "nonpayable",
108
+ inputs: [
109
+ { name: "traceIdHash", type: "bytes32" },
110
+ { name: "contentHash", type: "bytes32" },
111
+ { name: "subject", type: "bytes32" },
112
+ { name: "uri", type: "string" },
113
+ ],
114
+ outputs: [],
115
+ },
116
+ {
117
+ type: "function",
118
+ name: "isRecorded",
119
+ stateMutability: "view",
120
+ inputs: [{ name: "traceIdHash", type: "bytes32" }],
121
+ outputs: [{ type: "bool" }],
122
+ },
123
+ {
124
+ type: "function",
125
+ name: "getReceipt",
126
+ stateMutability: "view",
127
+ inputs: [{ name: "traceIdHash", type: "bytes32" }],
128
+ outputs: [
129
+ {
130
+ name: "receipt",
131
+ type: "tuple",
132
+ components: [
133
+ { name: "traceIdHash", type: "bytes32" },
134
+ { name: "contentHash", type: "bytes32" },
135
+ { name: "subject", type: "bytes32" },
136
+ { name: "uri", type: "string" },
137
+ { name: "recordedAt", type: "uint64" },
138
+ { name: "recorder", type: "address" },
139
+ ],
140
+ },
141
+ ],
142
+ },
143
+ ];
144
+
145
+ export const PolicyEngine_ABI = [
146
+ {
147
+ type: "function",
148
+ name: "setMaxValuePerTx",
149
+ stateMutability: "nonpayable",
150
+ inputs: [{ name: "v", type: "uint256" }],
151
+ outputs: [],
152
+ },
153
+ {
154
+ type: "function",
155
+ name: "setModuleAllowed",
156
+ stateMutability: "nonpayable",
157
+ inputs: [{ name: "module", type: "address" }, { name: "allowed", type: "bool" }],
158
+ outputs: [],
159
+ },
160
+ {
161
+ type: "function",
162
+ name: "setTargetAllowed",
163
+ stateMutability: "nonpayable",
164
+ inputs: [{ name: "target", type: "address" }, { name: "allowed", type: "bool" }],
165
+ outputs: [],
166
+ },
167
+ {
168
+ type: "function",
169
+ name: "setSelectorAllowed",
170
+ stateMutability: "nonpayable",
171
+ inputs: [
172
+ { name: "target", type: "address" },
173
+ { name: "selector", type: "bytes4" },
174
+ { name: "allowed", type: "bool" },
175
+ ],
176
+ outputs: [],
177
+ },
178
+ {
179
+ type: "function",
180
+ name: "preCheck",
181
+ stateMutability: "view",
182
+ inputs: [
183
+ { name: "module", type: "address" },
184
+ { name: "target", type: "address" },
185
+ { name: "selector", type: "bytes4" },
186
+ { name: "value", type: "uint256" },
187
+ ],
188
+ outputs: [],
189
+ },
190
+ ];
191
+
192
+ export const AgentAccount_ABI = [
193
+ {
194
+ type: "function",
195
+ name: "executeSkill",
196
+ stateMutability: "payable",
197
+ inputs: [
198
+ { name: "module", type: "address" },
199
+ { name: "input", type: "bytes" },
200
+ { name: "value", type: "uint256" },
201
+ { name: "traceIdHash", type: "bytes32" },
202
+ { name: "subjectHash", type: "bytes32" },
203
+ { name: "uri", type: "string" },
204
+ ],
205
+ outputs: [{ name: "output", type: "bytes" }],
206
+ },
207
+ {
208
+ type: "function",
209
+ name: "setPolicyEngine",
210
+ stateMutability: "nonpayable",
211
+ inputs: [{ name: "policyEngine", type: "address" }],
212
+ outputs: [],
213
+ },
214
+ {
215
+ type: "function",
216
+ name: "setReceiptRegistry",
217
+ stateMutability: "nonpayable",
218
+ inputs: [{ name: "receiptRegistry", type: "address" }],
219
+ outputs: [],
220
+ },
221
+ ];
222
+
223
+ export const PodVault_ABI = [
224
+ {
225
+ type: "function",
226
+ name: "totalShares",
227
+ stateMutability: "view",
228
+ inputs: [],
229
+ outputs: [{ name: "", type: "uint256" }],
230
+ },
231
+ {
232
+ type: "function",
233
+ name: "totalReleasedNative",
234
+ stateMutability: "view",
235
+ inputs: [],
236
+ outputs: [{ name: "", type: "uint256" }],
237
+ },
238
+ {
239
+ type: "function",
240
+ name: "memberCount",
241
+ stateMutability: "view",
242
+ inputs: [],
243
+ outputs: [{ name: "", type: "uint256" }],
244
+ },
245
+ {
246
+ type: "function",
247
+ name: "memberAt",
248
+ stateMutability: "view",
249
+ inputs: [{ name: "idx", type: "uint256" }],
250
+ outputs: [{ name: "", type: "address" }],
251
+ },
252
+ {
253
+ type: "function",
254
+ name: "shares",
255
+ stateMutability: "view",
256
+ inputs: [{ name: "member", type: "address" }],
257
+ outputs: [{ name: "", type: "uint256" }],
258
+ },
259
+ {
260
+ type: "function",
261
+ name: "pendingNative",
262
+ stateMutability: "view",
263
+ inputs: [{ name: "member", type: "address" }],
264
+ outputs: [{ name: "", type: "uint256" }],
265
+ },
266
+ {
267
+ type: "function",
268
+ name: "releaseNative",
269
+ stateMutability: "nonpayable",
270
+ inputs: [{ name: "member", type: "address" }],
271
+ outputs: [],
272
+ },
273
+ {
274
+ type: "function",
275
+ name: "pendingToken",
276
+ stateMutability: "view",
277
+ inputs: [
278
+ { name: "token", type: "address" },
279
+ { name: "member", type: "address" },
280
+ ],
281
+ outputs: [{ name: "", type: "uint256" }],
282
+ },
283
+ {
284
+ type: "function",
285
+ name: "releaseToken",
286
+ stateMutability: "nonpayable",
287
+ inputs: [
288
+ { name: "token", type: "address" },
289
+ { name: "member", type: "address" },
290
+ ],
291
+ outputs: [],
292
+ },
293
+ ];
294
+
@@ -0,0 +1,27 @@
1
+ import fs from "node:fs";
2
+ import { keccak256, toHex } from "viem";
3
+
4
+ export function stableJsonStringify(obj) {
5
+ if (obj === null || typeof obj !== "object") return JSON.stringify(obj);
6
+ if (Array.isArray(obj)) return `[${obj.map(stableJsonStringify).join(",")}]`;
7
+ const keys = Object.keys(obj).sort();
8
+ return `{${keys.map((k) => `${JSON.stringify(k)}:${stableJsonStringify(obj[k])}`).join(",")}}`;
9
+ }
10
+
11
+ export function computeSkillcardContentHash(skillcardObj) {
12
+ const canon = stableJsonStringify(skillcardObj);
13
+ return keccak256(toHex(canon));
14
+ }
15
+
16
+ export function computeSkillVersionHash(versionString) {
17
+ const v = String(versionString || "").trim() || "0.0.0";
18
+ return keccak256(toHex(v));
19
+ }
20
+
21
+ export function readSkillcardJson(filePath) {
22
+ const raw = fs.readFileSync(filePath, "utf8");
23
+ const obj = JSON.parse(raw);
24
+ if (!obj || typeof obj !== "object") throw new Error("invalid skillcard json");
25
+ return obj;
26
+ }
27
+
@@ -0,0 +1,169 @@
1
+ /**
2
+ * ApeClaw telemetry server -- modular entry point.
3
+ *
4
+ * Replaces the monolithic telemetry-server.mjs.
5
+ * All route logic lives in routes/, middleware in middleware/, storage in storage/.
6
+ */
7
+
8
+ import http from "node:http";
9
+ import fs from "node:fs";
10
+ import { POLICY_PATH } from "../lib/paths.mjs";
11
+ import { initStorage } from "./storage/index.mjs";
12
+ import { initSseBroadcast, closeAllClients } from "./sse.mjs";
13
+ import { handleCorsPreflightOrSetHeaders } from "./middleware/cors.mjs";
14
+ import { checkRateLimit } from "./middleware/rate-limit.mjs";
15
+ import logger from "./logger.mjs";
16
+
17
+ import { handleHealth } from "./routes/health.mjs";
18
+ import { handleEventsSse, handleEventsBacklog, handlePostEvent } from "./routes/events.mjs";
19
+ import {
20
+ handleSkillsSearch, handleSkillsGet, handleSkillsStats,
21
+ handleSkillcardsUserGet, handleSkillcardsAuthCheck,
22
+ handleSkillcardsUserAdd, handleSkillcardsUserDelete,
23
+ handleSkillcardsUserMarkOnchain, handleSkillcardFile,
24
+ } from "./routes/skills.mjs";
25
+ import {
26
+ handleClawbotsList, handleClawbotsVerify,
27
+ handleInviteCreate, handleClawbotsRegister,
28
+ } from "./routes/clawbots.mjs";
29
+ import {
30
+ handleChatStream, handleChatGet, handleChatRooms,
31
+ handleChatPost, handleChatReact,
32
+ } from "./routes/chat.mjs";
33
+ import { handleV2ReceiptGet, handleV2Config } from "./routes/v2.mjs";
34
+ import { handlePodStatus, handlePodStop } from "./routes/pod.mjs";
35
+ import {
36
+ handleCreateQuote, handleGetQuote, handlePatchQuote, handleQuotesSpendToday,
37
+ handleCreateBridgeRequest, handleGetBridgeRequest, handlePatchBridgeRequest, handleBridgeSpendToday,
38
+ } from "./routes/quotes.mjs";
39
+ import {
40
+ handleAllowlist, handlePolicy, handleRewrite,
41
+ handleIndex, handleStaticFile,
42
+ } from "./routes/static.mjs";
43
+
44
+ const PORT = Number(process.env.APE_CLAW_UI_PORT || 8787);
45
+ const BIND_HOST = String(process.env.APE_CLAW_BIND_HOST || "").trim();
46
+
47
+ const RL_READ = { limit: 60, windowMs: 60_000, keyPrefix: "read" };
48
+ const RL_WRITE = { limit: 10, windowMs: 60_000, keyPrefix: "write" };
49
+ const RL_AUTH = { limit: 5, windowMs: 60_000, keyPrefix: "auth" };
50
+
51
+ // ── Startup validation ──
52
+ if (!fs.existsSync(POLICY_PATH)) {
53
+ logger.warn({ path: POLICY_PATH }, "config/policy.json not found — some features may not work");
54
+ }
55
+ if (!process.env.OPENSEA_API_KEY) {
56
+ logger.info("OPENSEA_API_KEY not set — allowlist icons will be unavailable");
57
+ }
58
+
59
+ initStorage();
60
+ initSseBroadcast();
61
+ logger.info("Storage initialized, SSE broadcast active");
62
+
63
+ function safeHandler(fn) {
64
+ return (req, res, ...args) => {
65
+ try {
66
+ const result = fn(req, res, ...args);
67
+ if (result && typeof result.catch === "function") {
68
+ result.catch((err) => {
69
+ logger.error({ err, url: req.url, method: req.method }, "Unhandled route error");
70
+ if (!res.headersSent) { res.writeHead(500, { "content-type": "application/json" }); }
71
+ if (!res.writableEnded) { res.end(JSON.stringify({ error: "internal server error" })); }
72
+ });
73
+ }
74
+ } catch (err) {
75
+ logger.error({ err, url: req.url, method: req.method }, "Sync route error");
76
+ if (!res.headersSent) { res.writeHead(500, { "content-type": "application/json" }); }
77
+ if (!res.writableEnded) { res.end(JSON.stringify({ error: "internal server error" })); }
78
+ }
79
+ };
80
+ }
81
+
82
+ const server = http.createServer((req, res) => {
83
+ if (!req.url) return res.end("bad request");
84
+ const reqUrl = new URL(req.url, `http://localhost:${PORT}`);
85
+ const pathname = reqUrl.pathname;
86
+
87
+ if (handleCorsPreflightOrSetHeaders(req, res)) return;
88
+
89
+ if (pathname.startsWith("/api/")) {
90
+ const isAuth = pathname.startsWith("/api/clawbots/register") || pathname.startsWith("/api/clawbots/verify");
91
+ const isWrite = req.method === "POST" || req.method === "PATCH";
92
+ const rl = isAuth ? RL_AUTH : isWrite ? RL_WRITE : RL_READ;
93
+ if (checkRateLimit(req, res, rl)) return;
94
+ }
95
+
96
+ // ── SSE streams ──
97
+ if (pathname === "/events") return safeHandler(handleEventsSse)(req, res);
98
+ if (pathname === "/events/backlog") return safeHandler(handleEventsBacklog)(req, res, reqUrl);
99
+
100
+ // ── API routes ──
101
+ if (pathname === "/api/health") return safeHandler(handleHealth)(req, res);
102
+ if (pathname === "/api/allowlist") return safeHandler(handleAllowlist)(req, res);
103
+ if (pathname === "/api/policy") return safeHandler(handlePolicy)(req, res);
104
+ if (pathname === "/api/skillcards/user" && req.method === "GET") return safeHandler(handleSkillcardsUserGet)(req, res);
105
+ if (pathname === "/api/skills/search" && req.method === "GET") return safeHandler(handleSkillsSearch)(req, res, reqUrl);
106
+ if (pathname === "/api/skills/get" && req.method === "GET") return safeHandler(handleSkillsGet)(req, res, reqUrl);
107
+ if (pathname === "/api/skills/stats" && req.method === "GET") return safeHandler(handleSkillsStats)(req, res);
108
+ if (pathname === "/api/skillcards/user/auth-check" && req.method === "GET") return safeHandler(handleSkillcardsAuthCheck)(req, res);
109
+ if (pathname === "/api/skillcards/user/add" && req.method === "POST") return safeHandler(handleSkillcardsUserAdd)(req, res);
110
+ if (pathname === "/api/skillcards/user/delete" && req.method === "POST") return safeHandler(handleSkillcardsUserDelete)(req, res);
111
+ if (pathname === "/api/skillcards/user/mark-onchain" && req.method === "POST") return safeHandler(handleSkillcardsUserMarkOnchain)(req, res);
112
+ if (pathname.startsWith("/skillcards/") && req.method === "GET") return safeHandler(handleSkillcardFile)(req, res, pathname);
113
+ if (pathname === "/api/clawbots" && req.method === "GET") return safeHandler(handleClawbotsList)(req, res);
114
+ if (pathname === "/api/clawbots/verify" && req.method === "POST") return safeHandler(handleClawbotsVerify)(req, res);
115
+ if (pathname === "/api/invites/create" && req.method === "POST") return safeHandler(handleInviteCreate)(req, res);
116
+ if (pathname === "/api/clawbots/register" && req.method === "POST") return safeHandler(handleClawbotsRegister)(req, res);
117
+ if (pathname === "/api/events" && req.method === "POST") return safeHandler(handlePostEvent)(req, res);
118
+ if (pathname === "/api/chat/stream") return safeHandler(handleChatStream)(req, res, reqUrl);
119
+ if (pathname === "/api/chat" && req.method === "GET") return safeHandler(handleChatGet)(req, res, reqUrl);
120
+ if (pathname === "/api/chat/rooms" && req.method === "GET") return safeHandler(handleChatRooms)(req, res, reqUrl);
121
+ if (pathname === "/api/chat" && req.method === "POST") return safeHandler(handleChatPost)(req, res, reqUrl);
122
+ if (pathname === "/api/chat/react" && req.method === "POST") return safeHandler(handleChatReact)(req, res, reqUrl);
123
+ if (pathname === "/api/v2/receipt/get" && req.method === "GET") return safeHandler(handleV2ReceiptGet)(req, res, reqUrl);
124
+ if (pathname === "/api/v2/config" && req.method === "GET") return safeHandler(handleV2Config)(req, res);
125
+ if (pathname === "/api/pod/status" && req.method === "GET") return safeHandler(handlePodStatus)(req, res);
126
+ if (pathname === "/api/pod/stop" && req.method === "POST") return safeHandler(handlePodStop)(req, res);
127
+
128
+ // ── Quote & bridge-request state APIs (M2) ──
129
+ if (pathname === "/api/quotes" && req.method === "POST") return safeHandler(handleCreateQuote)(req, res);
130
+ if (pathname === "/api/quotes/spend-today" && req.method === "GET") return safeHandler(handleQuotesSpendToday)(req, res);
131
+ if (pathname.startsWith("/api/quotes/") && req.method === "GET") return safeHandler(handleGetQuote)(req, res, reqUrl);
132
+ if (pathname.startsWith("/api/quotes/") && req.method === "PATCH") return safeHandler(handlePatchQuote)(req, res, reqUrl);
133
+ if (pathname === "/api/bridge-requests" && req.method === "POST") return safeHandler(handleCreateBridgeRequest)(req, res);
134
+ if (pathname === "/api/bridge-requests/spend-today" && req.method === "GET") return safeHandler(handleBridgeSpendToday)(req, res);
135
+ if (pathname.startsWith("/api/bridge-requests/") && req.method === "GET") return safeHandler(handleGetBridgeRequest)(req, res, reqUrl);
136
+ if (pathname.startsWith("/api/bridge-requests/") && req.method === "PATCH") return safeHandler(handlePatchBridgeRequest)(req, res, reqUrl);
137
+
138
+ // ── Static / rewrite ──
139
+ if (handleRewrite(req, res, pathname)) return;
140
+ if (pathname === "/" || pathname === "/index.html") return safeHandler(handleIndex)(req, res);
141
+ if (handleStaticFile(req, res, pathname)) return;
142
+
143
+ res.writeHead(404);
144
+ res.end("not found");
145
+ });
146
+
147
+ // ── Graceful shutdown ──
148
+ let shuttingDown = false;
149
+ function shutdown(signal) {
150
+ if (shuttingDown) return;
151
+ shuttingDown = true;
152
+ logger.info({ signal }, "Shutting down...");
153
+ closeAllClients();
154
+ server.close(() => {
155
+ logger.info("Server closed.");
156
+ process.exit(0);
157
+ });
158
+ setTimeout(() => { logger.warn("Forceful shutdown after timeout."); process.exit(1); }, 10_000).unref();
159
+ }
160
+ process.on("SIGTERM", () => shutdown("SIGTERM"));
161
+ process.on("SIGINT", () => shutdown("SIGINT"));
162
+
163
+ server.listen(PORT, BIND_HOST || undefined, () => {
164
+ logger.info({ port: PORT, bind: BIND_HOST || "0.0.0.0", corsOrigins: process.env.APE_CLAW_CORS_ORIGINS || "(default)" }, "Server listening");
165
+ console.log(`ape-claw telemetry server listening on http://localhost:${PORT}`);
166
+ console.log(`SSE stream: http://localhost:${PORT}/events`);
167
+ });
168
+
169
+ export { server };
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Structured logger for the ApeClaw server.
3
+ *
4
+ * Uses pino for JSON output in production and pretty-print in dev.
5
+ * CLI keeps using console (pino would interfere with --json output).
6
+ */
7
+
8
+ import pino from "pino";
9
+
10
+ const isDev = String(process.env.NODE_ENV || "").toLowerCase() !== "production";
11
+
12
+ const logger = pino({
13
+ level: process.env.LOG_LEVEL || (isDev ? "debug" : "info"),
14
+ ...(isDev ? { transport: { target: "pino/file", options: { destination: 1 } } } : {}),
15
+ });
16
+
17
+ export default logger;
18
+
19
+ export function childLogger(bindings) {
20
+ return logger.child(bindings);
21
+ }
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Auth middleware for route handlers.
3
+ *
4
+ * Provides two auth modes:
5
+ * - requireSkillWriteAuth: admin key OR authenticated clawbot
6
+ * - resolveChatAuth: clawbot auth OR Moltbook identity verification
7
+ */
8
+
9
+ import { verifyClawbot } from "../../lib/clawbots.mjs";
10
+
11
+ const MOLTBOOK_API_BASE = String(process.env.MOLTBOOK_API_BASE || "https://www.moltbook.com/api/v1").replace(/\/+$/, "");
12
+ const MOLTBOOK_APP_KEY = String(process.env.MOLTBOOK_APP_KEY || "").trim();
13
+ const REGISTRATION_KEY = String(process.env.APE_CLAW_REGISTRATION_KEY || "").trim();
14
+
15
+ export function getRegistrationKey() { return REGISTRATION_KEY; }
16
+ export function getMoltbookAppKey() { return MOLTBOOK_APP_KEY; }
17
+ export function getMoltbookApiBase() { return MOLTBOOK_API_BASE; }
18
+
19
+ export function requireSkillWriteAuth(req) {
20
+ const adminKey = String(req.headers["x-registration-key"] || "").trim();
21
+ if (adminKey && REGISTRATION_KEY && adminKey === REGISTRATION_KEY) {
22
+ return { ok: true, mode: "admin", agentId: null };
23
+ }
24
+ const agentId = String(req.headers["x-agent-id"] || "").trim();
25
+ const agentToken = String(req.headers["x-agent-token"] || "").trim();
26
+ if (agentId && agentToken) {
27
+ try {
28
+ const v = verifyClawbot({ agentId, agentToken });
29
+ if (v?.verified) return { ok: true, mode: "agent", agentId };
30
+ } catch {}
31
+ }
32
+ return { ok: false, mode: "none", agentId: null };
33
+ }
34
+
35
+ export async function verifyMoltbookIdentity(identityToken) {
36
+ const token = String(identityToken || "").trim();
37
+ if (!token) return { verified: false, reason: "missing identity token" };
38
+ if (!MOLTBOOK_APP_KEY) return { verified: false, reason: "MOLTBOOK_APP_KEY not configured on backend" };
39
+ try {
40
+ const r = await fetch(`${MOLTBOOK_API_BASE}/agents/verify-identity`, {
41
+ method: "POST",
42
+ headers: { "content-type": "application/json", "x-moltbook-app-key": MOLTBOOK_APP_KEY },
43
+ body: JSON.stringify({ token }),
44
+ });
45
+ const data = await r.json().catch(() => ({}));
46
+ if (!r.ok) return { verified: false, reason: data?.error || `identity verify failed (${r.status})` };
47
+ if (!data?.valid || !data?.agent) return { verified: false, reason: "identity token invalid" };
48
+ return { verified: true, agent: data.agent };
49
+ } catch (err) {
50
+ return { verified: false, reason: err.message || "identity verification request failed" };
51
+ }
52
+ }
53
+
54
+ export async function resolveChatAuth(req, body) {
55
+ const agentId = body.agentId || req.headers["x-agent-id"] || "";
56
+ const agentToken = body.agentToken || req.headers["x-agent-token"] || "";
57
+ const identityToken = body.identityToken || req.headers["x-moltbook-identity"] || "";
58
+
59
+ if (identityToken) {
60
+ const identity = await verifyMoltbookIdentity(identityToken);
61
+ if (!identity.verified) return { ok: false, status: 403, error: "identity verify failed", reason: identity.reason };
62
+ const agent = identity.agent || {};
63
+ return {
64
+ ok: true,
65
+ auth: {
66
+ id: String(agent.name || agent.id || "moltbook-agent"),
67
+ name: String(agent.name || agent.id || "Moltbook Agent"),
68
+ provider: "moltbook",
69
+ meta: { karma: Number(agent.karma || 0), claimed: Boolean(agent.is_claimed) },
70
+ },
71
+ };
72
+ }
73
+
74
+ if (!agentId || !agentToken) {
75
+ return { ok: false, status: 401, error: "missing credentials: provide agentId+agentToken or identityToken" };
76
+ }
77
+ const verification = verifyClawbot({ agentId, agentToken });
78
+ if (!verification.verified) {
79
+ return { ok: false, status: 403, error: "not verified", reason: verification.reason };
80
+ }
81
+ return {
82
+ ok: true,
83
+ auth: {
84
+ id: agentId,
85
+ name: verification.agent?.name || agentId,
86
+ provider: "clawbot",
87
+ meta: {},
88
+ },
89
+ };
90
+ }
@@ -0,0 +1,35 @@
1
+ const MAX_BODY_BYTES = 1024 * 1024; // 1 MB
2
+
3
+ /**
4
+ * Collects the full request body up to MAX_BODY_BYTES.
5
+ * Returns the raw string, or null if the body exceeds the limit
6
+ * (in which case a 413 response is sent automatically).
7
+ */
8
+ export function collectBody(req, res, maxBytes = MAX_BODY_BYTES) {
9
+ return new Promise((resolve, reject) => {
10
+ const chunks = [];
11
+ let size = 0;
12
+
13
+ req.on("data", (chunk) => {
14
+ size += chunk.length;
15
+ if (size > maxBytes) {
16
+ req.destroy();
17
+ if (!res.headersSent) {
18
+ res.writeHead(413, { "content-type": "application/json" });
19
+ res.end(JSON.stringify({ error: "request body too large" }));
20
+ }
21
+ resolve(null);
22
+ return;
23
+ }
24
+ chunks.push(chunk);
25
+ });
26
+
27
+ req.on("end", () => {
28
+ resolve(Buffer.concat(chunks).toString("utf8"));
29
+ });
30
+
31
+ req.on("error", (err) => {
32
+ reject(err);
33
+ });
34
+ });
35
+ }
@@ -0,0 +1,33 @@
1
+ const ALLOWED_ORIGINS = new Set([
2
+ "https://apeclaw.ai",
3
+ "https://www.apeclaw.ai",
4
+ "http://localhost:8787",
5
+ "http://localhost:3000",
6
+ "http://127.0.0.1:8787",
7
+ ]);
8
+
9
+ function originAllowed(origin) {
10
+ if (!origin) return true;
11
+ if (ALLOWED_ORIGINS.has(origin)) return true;
12
+ if (/^https?:\/\/(localhost|127\.0\.0\.1)(:\d+)?$/.test(origin)) return true;
13
+ if (/\.vercel\.app$/.test(origin)) return true;
14
+ return false;
15
+ }
16
+
17
+ export function handleCorsPreflightOrSetHeaders(req, res) {
18
+ const origin = String(req.headers.origin || "").trim();
19
+ const allowed = originAllowed(origin);
20
+ const reflect = allowed && origin ? origin : "*";
21
+
22
+ res.setHeader("access-control-allow-origin", reflect);
23
+ res.setHeader("access-control-allow-methods", "GET, POST, PATCH, OPTIONS");
24
+ res.setHeader("access-control-allow-headers", "content-type, x-agent-id, x-agent-token, x-registration-key, x-moltbook-identity, x-api-key");
25
+ res.setHeader("access-control-max-age", "86400");
26
+
27
+ if (req.method === "OPTIONS") {
28
+ res.writeHead(204);
29
+ res.end();
30
+ return true;
31
+ }
32
+ return false;
33
+ }