From 05019d0de869327b7efbcbf7b0452fb8f6b6fa9e Mon Sep 17 00:00:00 2001 From: 0xMosas Date: Sat, 14 Mar 2026 10:14:12 +0100 Subject: [PATCH 01/31] feat: add write queue for serializing file operations Initialize a promise chain that will be used to serialize all read-modify-write operations on the events file. This prevents the race condition where concurrent webhook deliveries could overwrite each other's data. Refs #241 --- chainhook/server.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/chainhook/server.js b/chainhook/server.js index 68ba91c9..6e588ebd 100644 --- a/chainhook/server.js +++ b/chainhook/server.js @@ -15,6 +15,8 @@ if (!existsSync(DATA_DIR)) { mkdirSync(DATA_DIR, { recursive: true }); } +let writeQueue = Promise.resolve(); + function loadEvents() { if (!existsSync(DB_FILE)) return []; try { From 98befada60349f0ba03ba97d08854eb9970b216f Mon Sep 17 00:00:00 2001 From: 0xMosas Date: Sat, 14 Mar 2026 10:14:12 +0100 Subject: [PATCH 02/31] feat: add withEventLock function for queued operations withEventLock chains the provided function onto the write queue, ensuring only one read-modify-write cycle runs at a time. The second argument to .then() ensures the queue continues even if a previous operation fails. --- chainhook/server.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/chainhook/server.js b/chainhook/server.js index 6e588ebd..ec15086b 100644 --- a/chainhook/server.js +++ b/chainhook/server.js @@ -17,6 +17,11 @@ if (!existsSync(DATA_DIR)) { let writeQueue = Promise.resolve(); +function withEventLock(fn) { + writeQueue = writeQueue.then(fn, fn); + return writeQueue; +} + function loadEvents() { if (!existsSync(DB_FILE)) return []; try { From 6e2b19a5ec6fb0426357aec44f7636b3e9de6e31 Mon Sep 17 00:00:00 2001 From: 0xMosas Date: Sat, 14 Mar 2026 10:14:12 +0100 Subject: [PATCH 03/31] refactor: export withEventLock for testing --- chainhook/server.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chainhook/server.js b/chainhook/server.js index ec15086b..e70750e9 100644 --- a/chainhook/server.js +++ b/chainhook/server.js @@ -134,7 +134,7 @@ function parseTipEvent(event) { }; } -export { parseBody, extractEvents, parseTipEvent, sendJson }; +export { parseBody, extractEvents, parseTipEvent, sendJson, withEventLock }; const server = http.createServer(async (req, res) => { const url = new URL(req.url, `http://localhost:${PORT}`); From 8ad02f2c98411602efdb0f70351780509d196deb Mon Sep 17 00:00:00 2001 From: 0xMosas Date: Sat, 14 Mar 2026 10:14:13 +0100 Subject: [PATCH 04/31] fix: wrap event storage in withEventLock to prevent race conditions The read-modify-write cycle (loadEvents -> push -> saveEvents) is now serialized through the write queue. Concurrent webhook deliveries will execute their file operations one at a time, preventing the second write from overwriting events added by the first. Closes #241 --- chainhook/server.js | 31 ++++++++++++++++--------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/chainhook/server.js b/chainhook/server.js index e70750e9..3c21357d 100644 --- a/chainhook/server.js +++ b/chainhook/server.js @@ -167,23 +167,24 @@ const server = http.createServer(async (req, res) => { const payload = await parseBody(req); const newEvents = extractEvents(payload); if (newEvents.length > 0) { - const stored = loadEvents(); - - // Check for timelock bypass events - for (const evt of newEvents) { - const detection = detectBypass(evt, stored.slice(-50)); - if (detection.isBypass) { - console.warn(formatBypassAlert(detection, evt)); - } - const adminEvt = parseAdminEvent(evt); - if (adminEvt) { - console.log(`Admin event: ${adminEvt.eventType} at block ${adminEvt.blockHeight}`); + await withEventLock(() => { + const stored = loadEvents(); + + for (const evt of newEvents) { + const detection = detectBypass(evt, stored.slice(-50)); + if (detection.isBypass) { + console.warn(formatBypassAlert(detection, evt)); + } + const adminEvt = parseAdminEvent(evt); + if (adminEvt) { + console.log(`Admin event: ${adminEvt.eventType} at block ${adminEvt.blockHeight}`); + } } - } - stored.push(...newEvents); - saveEvents(stored); - console.log(`Indexed ${newEvents.length} events (total: ${stored.length})`); + stored.push(...newEvents); + saveEvents(stored); + console.log(`Indexed ${newEvents.length} events (total: ${stored.length})`); + }); } return sendJson(res, 200, { ok: true, indexed: newEvents.length }); } catch (err) { From 6dd133ee5e2223abe8238093b60eaae977b725d8 Mon Sep 17 00:00:00 2001 From: 0xMosas Date: Sat, 14 Mar 2026 10:15:41 +0100 Subject: [PATCH 05/31] docs: add JSDoc to withEventLock --- chainhook/server.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/chainhook/server.js b/chainhook/server.js index 3c21357d..f1d338fb 100644 --- a/chainhook/server.js +++ b/chainhook/server.js @@ -17,6 +17,13 @@ if (!existsSync(DATA_DIR)) { let writeQueue = Promise.resolve(); +/** + * Serialize access to the events file. + * Chains the provided function onto a promise queue so that only one + * read-modify-write cycle runs at a time. + * @param {() => void} fn - Synchronous function that reads and writes events. + * @returns {Promise} + */ function withEventLock(fn) { writeQueue = writeQueue.then(fn, fn); return writeQueue; From 3f261d0bc92ea9494054f9db435c7be22ee5aabc Mon Sep 17 00:00:00 2001 From: 0xMosas Date: Sat, 14 Mar 2026 10:15:41 +0100 Subject: [PATCH 06/31] docs: add JSDoc to loadEvents --- chainhook/server.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/chainhook/server.js b/chainhook/server.js index f1d338fb..f4cfabff 100644 --- a/chainhook/server.js +++ b/chainhook/server.js @@ -29,6 +29,11 @@ function withEventLock(fn) { return writeQueue; } +/** + * Load all stored events from the JSON file. + * Returns an empty array if the file does not exist or is corrupted. + * @returns {Array} + */ function loadEvents() { if (!existsSync(DB_FILE)) return []; try { From e981f579cbccb1458b3838a94898d81c29e0ca50 Mon Sep 17 00:00:00 2001 From: 0xMosas Date: Sat, 14 Mar 2026 10:15:42 +0100 Subject: [PATCH 07/31] docs: add JSDoc to saveEvents with lock requirement note --- chainhook/server.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/chainhook/server.js b/chainhook/server.js index f4cfabff..bede7aef 100644 --- a/chainhook/server.js +++ b/chainhook/server.js @@ -43,6 +43,11 @@ function loadEvents() { } } +/** + * Persist events to the JSON file. + * Must only be called within withEventLock to avoid race conditions. + * @param {Array} events + */ function saveEvents(events) { writeFileSync(DB_FILE, JSON.stringify(events, null, 2)); } From ee2ae5f0afe377e6590fcac3ed00ebe1f6b74847 Mon Sep 17 00:00:00 2001 From: 0xMosas Date: Sat, 14 Mar 2026 10:15:42 +0100 Subject: [PATCH 08/31] docs: annotate DATA_DIR constant --- chainhook/server.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chainhook/server.js b/chainhook/server.js index bede7aef..6adf0d89 100644 --- a/chainhook/server.js +++ b/chainhook/server.js @@ -8,7 +8,7 @@ import { MAX_BODY_SIZE, isValidStacksAddress, sanitizeQueryInt } from "./validat const __dirname = dirname(fileURLToPath(import.meta.url)); const PORT = process.env.PORT || 3100; const AUTH_TOKEN = process.env.CHAINHOOK_AUTH_TOKEN || ""; -const DATA_DIR = join(__dirname, "data"); +const DATA_DIR = join(__dirname, "data"); // persistent storage directory const DB_FILE = join(DATA_DIR, "events.json"); if (!existsSync(DATA_DIR)) { From 3e60080ce55ceea660ce7cd5931c48799d7e4000 Mon Sep 17 00:00:00 2001 From: 0xMosas Date: Sat, 14 Mar 2026 10:15:42 +0100 Subject: [PATCH 09/31] docs: annotate DB_FILE constant --- chainhook/server.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chainhook/server.js b/chainhook/server.js index 6adf0d89..6bb39359 100644 --- a/chainhook/server.js +++ b/chainhook/server.js @@ -9,7 +9,7 @@ const __dirname = dirname(fileURLToPath(import.meta.url)); const PORT = process.env.PORT || 3100; const AUTH_TOKEN = process.env.CHAINHOOK_AUTH_TOKEN || ""; const DATA_DIR = join(__dirname, "data"); // persistent storage directory -const DB_FILE = join(DATA_DIR, "events.json"); +const DB_FILE = join(DATA_DIR, "events.json"); // JSON event store if (!existsSync(DATA_DIR)) { mkdirSync(DATA_DIR, { recursive: true }); From 824f8a44949d90a5dc3eb4eea7384eeb666e9b91 Mon Sep 17 00:00:00 2001 From: 0xMosas Date: Sat, 14 Mar 2026 10:16:51 +0100 Subject: [PATCH 10/31] docs: annotate PORT constant --- chainhook/server.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chainhook/server.js b/chainhook/server.js index 6bb39359..6c56999c 100644 --- a/chainhook/server.js +++ b/chainhook/server.js @@ -6,7 +6,7 @@ import { detectBypass, parseAdminEvent, formatBypassAlert } from "./bypass-detec import { MAX_BODY_SIZE, isValidStacksAddress, sanitizeQueryInt } from "./validation.js"; const __dirname = dirname(fileURLToPath(import.meta.url)); -const PORT = process.env.PORT || 3100; +const PORT = process.env.PORT || 3100; // default webhook listener port const AUTH_TOKEN = process.env.CHAINHOOK_AUTH_TOKEN || ""; const DATA_DIR = join(__dirname, "data"); // persistent storage directory const DB_FILE = join(DATA_DIR, "events.json"); // JSON event store From f33e7cb056a8d2e34558aa8ddc0a919b5a3e7a45 Mon Sep 17 00:00:00 2001 From: 0xMosas Date: Sat, 14 Mar 2026 10:16:52 +0100 Subject: [PATCH 11/31] docs: annotate AUTH_TOKEN constant --- chainhook/server.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chainhook/server.js b/chainhook/server.js index 6c56999c..149b4ef1 100644 --- a/chainhook/server.js +++ b/chainhook/server.js @@ -7,7 +7,7 @@ import { MAX_BODY_SIZE, isValidStacksAddress, sanitizeQueryInt } from "./validat const __dirname = dirname(fileURLToPath(import.meta.url)); const PORT = process.env.PORT || 3100; // default webhook listener port -const AUTH_TOKEN = process.env.CHAINHOOK_AUTH_TOKEN || ""; +const AUTH_TOKEN = process.env.CHAINHOOK_AUTH_TOKEN || ""; // optional bearer token const DATA_DIR = join(__dirname, "data"); // persistent storage directory const DB_FILE = join(DATA_DIR, "events.json"); // JSON event store From 85fcef64d239356fcffd6dcf53d74712afa4997b Mon Sep 17 00:00:00 2001 From: 0xMosas Date: Sat, 14 Mar 2026 10:16:52 +0100 Subject: [PATCH 12/31] fix: log errors from queued operations instead of swallowing Previously the catch argument (fn) would re-run the operation on failure. Now errors are logged and the queue continues. --- chainhook/server.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/chainhook/server.js b/chainhook/server.js index 149b4ef1..bc420365 100644 --- a/chainhook/server.js +++ b/chainhook/server.js @@ -25,7 +25,9 @@ let writeQueue = Promise.resolve(); * @returns {Promise} */ function withEventLock(fn) { - writeQueue = writeQueue.then(fn, fn); + writeQueue = writeQueue.then(fn).catch((err) => { + console.error('Event lock operation failed:', err.message); + }); return writeQueue; } From a06dc4b050050fe5e2d13c183fa5f7246d8f99a0 Mon Sep 17 00:00:00 2001 From: 0xMosas Date: Sat, 14 Mar 2026 10:16:52 +0100 Subject: [PATCH 13/31] test: add __test_resetQueue helper for test isolation --- chainhook/server.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/chainhook/server.js b/chainhook/server.js index bc420365..c950ff8f 100644 --- a/chainhook/server.js +++ b/chainhook/server.js @@ -153,7 +153,11 @@ function parseTipEvent(event) { }; } -export { parseBody, extractEvents, parseTipEvent, sendJson, withEventLock }; +function __test_resetQueue() { + writeQueue = Promise.resolve(); +} + +export { parseBody, extractEvents, parseTipEvent, sendJson, withEventLock, __test_resetQueue }; const server = http.createServer(async (req, res) => { const url = new URL(req.url, `http://localhost:${PORT}`); From 04a86e874e47d4c36eec20dbfb7b64684cbc21a8 Mon Sep 17 00:00:00 2001 From: 0xMosas Date: Sat, 14 Mar 2026 10:16:52 +0100 Subject: [PATCH 14/31] docs: add comment for CORS preflight handler --- chainhook/server.js | 1 + 1 file changed, 1 insertion(+) diff --git a/chainhook/server.js b/chainhook/server.js index c950ff8f..db301df6 100644 --- a/chainhook/server.js +++ b/chainhook/server.js @@ -167,6 +167,7 @@ const server = http.createServer(async (req, res) => { res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS"); res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization"); + // Handle CORS preflight requests if (req.method === "OPTIONS") { res.writeHead(204); return res.end(); From dbfc13f88859a73dbcb4fc6761fd2ead06ea984e Mon Sep 17 00:00:00 2001 From: 0xMosas Date: Sat, 14 Mar 2026 10:16:52 +0100 Subject: [PATCH 15/31] docs: add comment for bearer token auth check --- chainhook/server.js | 1 + 1 file changed, 1 insertion(+) diff --git a/chainhook/server.js b/chainhook/server.js index db301df6..4f1d6710 100644 --- a/chainhook/server.js +++ b/chainhook/server.js @@ -180,6 +180,7 @@ const server = http.createServer(async (req, res) => { return sendJson(res, 413, { error: "payload too large" }); } + // Verify bearer token when AUTH_TOKEN is configured if (AUTH_TOKEN) { const auth = req.headers.authorization || ""; if (auth !== `Bearer ${AUTH_TOKEN}`) { From 0f815529f44a18d978e31fbc49e5b3bda2524a0b Mon Sep 17 00:00:00 2001 From: 0xMosas Date: Sat, 14 Mar 2026 10:16:52 +0100 Subject: [PATCH 16/31] docs: add route comment for POST chainhook events endpoint --- chainhook/server.js | 1 + 1 file changed, 1 insertion(+) diff --git a/chainhook/server.js b/chainhook/server.js index 4f1d6710..14d83d02 100644 --- a/chainhook/server.js +++ b/chainhook/server.js @@ -173,6 +173,7 @@ const server = http.createServer(async (req, res) => { return res.end(); } + // POST /api/chainhook/events -- ingest webhook payloads if (req.method === "POST" && path === "/api/chainhook/events") { // Early rejection based on Content-Length header const contentLength = parseInt(req.headers["content-length"], 10); From 9ff046c61f12fd67aa61e14eda2112a3ad190d8c Mon Sep 17 00:00:00 2001 From: 0xMosas Date: Sat, 14 Mar 2026 10:16:53 +0100 Subject: [PATCH 17/31] docs: add route comment for GET /api/tips endpoint --- chainhook/server.js | 1 + 1 file changed, 1 insertion(+) diff --git a/chainhook/server.js b/chainhook/server.js index 14d83d02..fb9288eb 100644 --- a/chainhook/server.js +++ b/chainhook/server.js @@ -222,6 +222,7 @@ const server = http.createServer(async (req, res) => { } } + // GET /api/tips -- paginated list of parsed tips if (req.method === "GET" && path === "/api/tips") { const limit = sanitizeQueryInt(url.searchParams.get("limit") || "20", 1, 100); const offset = sanitizeQueryInt(url.searchParams.get("offset") || "0", 0, Number.MAX_SAFE_INTEGER); From 4d9b43333a10edecf88313d00c7fcd7f850b9f36 Mon Sep 17 00:00:00 2001 From: 0xMosas Date: Sat, 14 Mar 2026 10:16:53 +0100 Subject: [PATCH 18/31] docs: add route comment for GET /api/tips/user endpoint --- chainhook/server.js | 1 + 1 file changed, 1 insertion(+) diff --git a/chainhook/server.js b/chainhook/server.js index fb9288eb..b915a134 100644 --- a/chainhook/server.js +++ b/chainhook/server.js @@ -243,6 +243,7 @@ const server = http.createServer(async (req, res) => { return sendJson(res, 200, { tips: paged, total: tips.length }); } + // GET /api/tips/user/:address -- tips sent or received by address if (req.method === "GET" && path.startsWith("/api/tips/user/")) { const address = path.split("/api/tips/user/")[1]; if (!isValidStacksAddress(address)) { From a1f8f7b317f76c95438f844d25cc2945fe5e5a0e Mon Sep 17 00:00:00 2001 From: 0xMosas Date: Sat, 14 Mar 2026 10:16:53 +0100 Subject: [PATCH 19/31] docs: add route comment for GET /api/tips/:id endpoint --- chainhook/server.js | 1 + 1 file changed, 1 insertion(+) diff --git a/chainhook/server.js b/chainhook/server.js index b915a134..2696718c 100644 --- a/chainhook/server.js +++ b/chainhook/server.js @@ -257,6 +257,7 @@ const server = http.createServer(async (req, res) => { return sendJson(res, 200, { tips, total: tips.length }); } + // GET /api/tips/:id -- single tip by numeric ID if (req.method === "GET" && path.match(/^\/api\/tips\/\d+$/)) { const tipId = parseInt(path.split("/api/tips/")[1], 10); if (isNaN(tipId) || tipId < 0) { From eae8859dcc506203a3804edc40d25027fd5c3d11 Mon Sep 17 00:00:00 2001 From: 0xMosas Date: Sat, 14 Mar 2026 10:16:53 +0100 Subject: [PATCH 20/31] docs: add route comment for GET /api/stats endpoint --- chainhook/server.js | 1 + 1 file changed, 1 insertion(+) diff --git a/chainhook/server.js b/chainhook/server.js index 2696718c..6675e137 100644 --- a/chainhook/server.js +++ b/chainhook/server.js @@ -269,6 +269,7 @@ const server = http.createServer(async (req, res) => { return sendJson(res, 200, tip); } + // GET /api/stats -- aggregate tip statistics if (req.method === "GET" && path === "/api/stats") { const allEvents = loadEvents(); const tips = allEvents.map(parseTipEvent).filter(Boolean); From 9b2490489d49da11fb96fdbe1ad563fa915c940d Mon Sep 17 00:00:00 2001 From: 0xMosas Date: Sat, 14 Mar 2026 10:16:53 +0100 Subject: [PATCH 21/31] docs: add route comment for GET /api/admin/events endpoint --- chainhook/server.js | 1 + 1 file changed, 1 insertion(+) diff --git a/chainhook/server.js b/chainhook/server.js index 6675e137..1244584b 100644 --- a/chainhook/server.js +++ b/chainhook/server.js @@ -284,6 +284,7 @@ const server = http.createServer(async (req, res) => { }); } + // GET /api/admin/events -- admin event log if (req.method === "GET" && path === "/api/admin/events") { const allEvents = loadEvents(); const adminEvents = allEvents From d5f2b96ffe36f858382d5bef87541f51462d4469 Mon Sep 17 00:00:00 2001 From: 0xMosas Date: Sat, 14 Mar 2026 10:16:53 +0100 Subject: [PATCH 22/31] docs: add route comment for GET /api/admin/bypasses endpoint --- chainhook/server.js | 1 + 1 file changed, 1 insertion(+) diff --git a/chainhook/server.js b/chainhook/server.js index 1244584b..3074fd60 100644 --- a/chainhook/server.js +++ b/chainhook/server.js @@ -294,6 +294,7 @@ const server = http.createServer(async (req, res) => { return sendJson(res, 200, { events: adminEvents, total: adminEvents.length }); } + // GET /api/admin/bypasses -- detected timelock bypass events if (req.method === "GET" && path === "/api/admin/bypasses") { const allEvents = loadEvents(); const bypasses = []; From 472463328257ec495d35b5a80610bd4b2b1f5af5 Mon Sep 17 00:00:00 2001 From: 0xMosas Date: Sat, 14 Mar 2026 10:16:53 +0100 Subject: [PATCH 23/31] feat: add Access-Control-Max-Age header to reduce preflight requests --- chainhook/server.js | 1 + 1 file changed, 1 insertion(+) diff --git a/chainhook/server.js b/chainhook/server.js index 3074fd60..eb46629c 100644 --- a/chainhook/server.js +++ b/chainhook/server.js @@ -166,6 +166,7 @@ const server = http.createServer(async (req, res) => { res.setHeader("Access-Control-Allow-Origin", "*"); res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS"); res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization"); + res.setHeader("Access-Control-Max-Age", "86400"); // Handle CORS preflight requests if (req.method === "OPTIONS") { From bc7eb28c2107f70adb8b33fb4763907ea206924e Mon Sep 17 00:00:00 2001 From: 0xMosas Date: Sat, 14 Mar 2026 10:16:54 +0100 Subject: [PATCH 24/31] docs: add comment for validation import --- chainhook/server.js | 1 + 1 file changed, 1 insertion(+) diff --git a/chainhook/server.js b/chainhook/server.js index eb46629c..16ec0d76 100644 --- a/chainhook/server.js +++ b/chainhook/server.js @@ -3,6 +3,7 @@ import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs"; import { join, dirname } from "node:path"; import { fileURLToPath } from "node:url"; import { detectBypass, parseAdminEvent, formatBypassAlert } from "./bypass-detection.js"; +// Input validation utilities import { MAX_BODY_SIZE, isValidStacksAddress, sanitizeQueryInt } from "./validation.js"; const __dirname = dirname(fileURLToPath(import.meta.url)); From 76436501e40700c992b20fedac11a23e515daba5 Mon Sep 17 00:00:00 2001 From: 0xMosas Date: Sat, 14 Mar 2026 10:16:54 +0100 Subject: [PATCH 25/31] docs: add comment for bypass-detection import --- chainhook/server.js | 1 + 1 file changed, 1 insertion(+) diff --git a/chainhook/server.js b/chainhook/server.js index 16ec0d76..c237bcb3 100644 --- a/chainhook/server.js +++ b/chainhook/server.js @@ -2,6 +2,7 @@ import http from "node:http"; import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs"; import { join, dirname } from "node:path"; import { fileURLToPath } from "node:url"; +// Timelock bypass detection utilities import { detectBypass, parseAdminEvent, formatBypassAlert } from "./bypass-detection.js"; // Input validation utilities import { MAX_BODY_SIZE, isValidStacksAddress, sanitizeQueryInt } from "./validation.js"; From 1547176a4f98377b34de51a092a379a228d56a7f Mon Sep 17 00:00:00 2001 From: 0xMosas Date: Sat, 14 Mar 2026 10:16:54 +0100 Subject: [PATCH 26/31] feat: include request path in 404 response for debugging --- chainhook/server.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chainhook/server.js b/chainhook/server.js index c237bcb3..d269a207 100644 --- a/chainhook/server.js +++ b/chainhook/server.js @@ -315,7 +315,7 @@ const server = http.createServer(async (req, res) => { return sendJson(res, 200, { bypasses, total: bypasses.length }); } - sendJson(res, 404, { error: "not found" }); + sendJson(res, 404, { error: "not found", path: path }); }); export { server }; From 95f94d311cdcdbcdc424b5deb1447f61b233ec8b Mon Sep 17 00:00:00 2001 From: 0xMosas Date: Sat, 14 Mar 2026 10:16:54 +0100 Subject: [PATCH 27/31] feat: log auth status on server startup --- chainhook/server.js | 1 + 1 file changed, 1 insertion(+) diff --git a/chainhook/server.js b/chainhook/server.js index d269a207..bef28684 100644 --- a/chainhook/server.js +++ b/chainhook/server.js @@ -328,5 +328,6 @@ const isMain = if (isMain) { server.listen(PORT, () => { console.log(`Chainhook callback server running on port ${PORT}`); + console.log(`Auth: ${AUTH_TOKEN ? "enabled" : "disabled"}`); }); } From 9f88f1ffc0ea42c3c8cb6c36bc96b7e0cb01c4f9 Mon Sep 17 00:00:00 2001 From: 0xMosas Date: Sat, 14 Mar 2026 10:16:54 +0100 Subject: [PATCH 28/31] docs: add description to chainhook package.json --- chainhook/package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/chainhook/package.json b/chainhook/package.json index 5edffce7..afaff562 100644 --- a/chainhook/package.json +++ b/chainhook/package.json @@ -9,5 +9,6 @@ }, "engines": { "node": ">=18" - } + }, + "description": "Chainhook webhook listener for TipStream on-chain events" } From fd04f061c39dffae4a0c1590805507b434a87461 Mon Sep 17 00:00:00 2001 From: 0xMosas Date: Sat, 14 Mar 2026 10:16:55 +0100 Subject: [PATCH 29/31] docs: explain why write queue is needed in single-threaded Node --- chainhook/server.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/chainhook/server.js b/chainhook/server.js index bef28684..6c7982d2 100644 --- a/chainhook/server.js +++ b/chainhook/server.js @@ -17,6 +17,9 @@ if (!existsSync(DATA_DIR)) { mkdirSync(DATA_DIR, { recursive: true }); } +// Serialized write queue. Node.js is single-threaded but async handlers +// can interleave between await points. This queue ensures file operations +// are atomic by chaining them sequentially. let writeQueue = Promise.resolve(); /** From 623cffb5534b1378116aa4b6ef2436c2a9b4c179 Mon Sep 17 00:00:00 2001 From: 0xMosas Date: Sat, 14 Mar 2026 10:16:55 +0100 Subject: [PATCH 30/31] refactor: export loadEvents for integration test access --- chainhook/server.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chainhook/server.js b/chainhook/server.js index 6c7982d2..633c0e6a 100644 --- a/chainhook/server.js +++ b/chainhook/server.js @@ -162,7 +162,7 @@ function __test_resetQueue() { writeQueue = Promise.resolve(); } -export { parseBody, extractEvents, parseTipEvent, sendJson, withEventLock, __test_resetQueue }; +export { parseBody, extractEvents, parseTipEvent, sendJson, withEventLock, loadEvents, __test_resetQueue }; const server = http.createServer(async (req, res) => { const url = new URL(req.url, `http://localhost:${PORT}`); From e06912875e8dadda1215dde5d5be7b59f8dc8c7f Mon Sep 17 00:00:00 2001 From: 0xMosas Date: Sat, 14 Mar 2026 10:16:55 +0100 Subject: [PATCH 31/31] style: ensure trailing newline in server.js