'use strict';
* Multi-endpoint webhook server — the routing + isolation rules that a single
* shared public surface lives or dies on:
*
* 1. one server, many endpoints, told apart by the id in the path (and bare
* `POST /` still means the pre-multi-endpoint `legacy` endpoint);
* 2. each endpoint is gated by ITS OWN secret, so revoking one caller cannot
* open or close another's door;
* 3. an unknown id is answered EXACTLY like a wrong secret, so the surface
* can't be walked to discover which webhooks exist;
* 4. the endpoint list is swappable at runtime — a revoked endpoint stops
* resolving on the very next request, with no restart (a restart would
* rotate the tunnel URL and break every OTHER caller);
* 5. per-endpoint rate limiting, so one noisy caller burns its own budget;
* 6. a held message answers 202 (accepted, not started) with its token.
*
* The HTTP handler is driven directly with stub req/res objects: `start()` opens
* a real tunnel, and a unit test must never reach the network.
*/
const test = require('node:test');
const assert = require('node:assert/strict');
const { EventEmitter } = require('node:events');
const loadTs = require('./load-ts.cjs');
const { WebhookServer, LEGACY_ENDPOINT_ID } = loadTs('src/main/webhook.ts');
const SECRET_A = 'a'.repeat(64);
const SECRET_B = 'b'.repeat(64);
const SCHEMA = JSON.stringify({
type: 'object',
required: ['message'],
properties: { message: { type: 'string' }, count: { type: 'number' } }
});
function endpoints() {
return [
{ id: 'alpha', name: 'Alpha', secret: SECRET_A, schema: SCHEMA },
{ id: LEGACY_ENDPOINT_ID, name: 'Default webhook', secret: SECRET_B, schema: SCHEMA }
];
}
function makeServer(overrides = {}) {
const seen = [];
const server = new WebhookServer({
port: 0,
endpoints: endpoints(),
onMessage: (msg, endpoint) => {
seen.push({ msg, endpoint });
if (overrides.pending) return { token: 'tok-pending', pending: true };
return { token: `tok-${endpoint.id}`, taskId: `webhook-${endpoint.id}`, pending: false };
},
lookupStatus: (token) =>
token === 'good-token' ? { status: 'todo', title: 'a card' } : null,
...overrides.opts
});
return { server, seen };
}
function request(server, { method = 'POST', url = '/', headers = {}, body = undefined }) {
return new Promise((resolve) => {
const req = new EventEmitter();
req.method = method;
req.url = url;
req.headers = headers;
req.destroy = () => { };
const res = {
writeHead(status) { res._status = status; return res; },
end(payload) {
let parsed = null;
try { parsed = payload ? JSON.parse(payload) : null; } catch { parsed = payload; }
resolve({ status: res._status, body: parsed });
}
};
server.handleRequest(req, res);
if (method === 'POST') {
if (body !== undefined) req.emit('data', Buffer.from(body));
req.emit('end');
}
});
}
const auth = (secret) => ({ 'x-md-webhook-secret': secret });
const post = (msg) => JSON.stringify(msg);
test('each endpoint is gated by its OWN secret', async () => {
const { server, seen } = makeServer();
const ok = await request(server, {
url: '/alpha', headers: auth(SECRET_A), body: post({ message: 'hello' })
});
assert.equal(ok.status, 200);
assert.equal(ok.body.token, 'tok-alpha');
assert.equal(seen[0].endpoint.id, 'alpha');
assert.deepEqual(Object.keys(seen[0].endpoint).sort(), ['id', 'name']);
const wrong = await request(server, {
url: '/alpha', headers: auth(SECRET_B), body: post({ message: 'hello' })
});
assert.equal(wrong.status, 401);
});
test('an unknown id is indistinguishable from a wrong secret', async () => {
const { server, seen } = makeServer();
const unknown = await request(server, {
url: '/does-not-exist', headers: auth(SECRET_A), body: post({ message: 'hi' })
});
const wrongSecret = await request(server, {
url: '/alpha', headers: auth(SECRET_B), body: post({ message: 'hi' })
});
assert.equal(unknown.status, wrongSecret.status);
assert.deepEqual(unknown.body, wrongSecret.body);
assert.deepEqual(unknown.body, { ok: false, error: 'unauthorized' });
assert.equal(seen.length, 0, 'neither request may reach the dispatch handler');
});
test('bare POST / still means the legacy endpoint', async () => {
const { server, seen } = makeServer();
const res = await request(server, {
url: '/', headers: auth(SECRET_B), body: post({ message: 'legacy caller' })
});
assert.equal(res.status, 200);
assert.equal(seen[0].endpoint.id, LEGACY_ENDPOINT_ID);
});
test('a nested path is not an endpoint', async () => {
const { server } = makeServer();
const res = await request(server, {
url: '/alpha/extra', headers: auth(SECRET_A), body: post({ message: 'hi' })
});
assert.equal(res.status, 401);
});
test('setEndpoints revokes one endpoint without touching the others', async () => {
const { server } = makeServer();
server.setEndpoints([{ id: LEGACY_ENDPOINT_ID, name: 'Default webhook', secret: SECRET_B, schema: SCHEMA }]);
const revoked = await request(server, {
url: '/alpha', headers: auth(SECRET_A), body: post({ message: 'hi' })
});
assert.equal(revoked.status, 401, 'the revoked endpoint stops resolving immediately');
const survivor = await request(server, {
url: '/legacy', headers: auth(SECRET_B), body: post({ message: 'hi' })
});
assert.equal(survivor.status, 200, 'every other endpoint is undisturbed');
});
test('the body is validated against THAT endpoint schema', async () => {
const { server } = makeServer();
const bad = await request(server, {
url: '/alpha', headers: auth(SECRET_A), body: post({ message: 'hi', count: 'seven' })
});
assert.equal(bad.status, 400);
assert.match(bad.body.error, /count/);
const missing = await request(server, {
url: '/alpha', headers: auth(SECRET_A), body: post({ title: 'no message here' })
});
assert.equal(missing.status, 400);
const junk = await request(server, {
url: '/alpha', headers: auth(SECRET_A), body: 'not json at all'
});
assert.equal(junk.status, 400);
assert.equal(junk.body.error, 'bad json');
});
test('the caller declaration of kind/from is passed through', async () => {
const { server, seen } = makeServer();
await request(server, {
url: '/alpha',
headers: auth(SECRET_A),
body: post({ message: 'ship it', kind: 'communication', from: 'ci-bot', title: 'build' })
});
assert.equal(seen[0].msg.kind, 'communication');
assert.equal(seen[0].msg.from, 'ci-bot');
assert.equal(seen[0].msg.title, 'build');
await request(server, {
url: '/alpha', headers: auth(SECRET_A), body: post({ message: 'x', kind: 'anything-goes' })
});
assert.equal(seen[1].msg.kind, undefined);
});
test('a held message answers 202 with its token and no task', async () => {
const { server } = makeServer({ pending: true });
const res = await request(server, {
url: '/alpha', headers: auth(SECRET_A), body: post({ message: 'do the thing' })
});
assert.equal(res.status, 202);
assert.equal(res.body.pending, true);
assert.equal(res.body.status, 'awaiting-approval');
assert.equal(res.body.token, 'tok-pending');
assert.equal(res.body.taskId, undefined);
});
test('GET is token-scoped, and answers 404 identically for every miss', async () => {
const { server } = makeServer();
const ok = await request(server, {
method: 'GET', url: '/alpha', headers: { 'x-md-webhook-token': 'good-token' }
});
assert.equal(ok.status, 200);
assert.equal(ok.body.status, 'todo');
const badToken = await request(server, {
method: 'GET', url: '/alpha', headers: { 'x-md-webhook-token': 'nope' }
});
const badEndpoint = await request(server, {
method: 'GET', url: '/ghost', headers: { 'x-md-webhook-token': 'good-token' }
});
assert.deepEqual(badToken.body, badEndpoint.body);
assert.equal(badToken.status, 404);
assert.equal(badEndpoint.status, 404);
const noToken = await request(server, { method: 'GET', url: '/alpha' });
assert.equal(noToken.status, 401);
});
test('the query-param token fallback still works', async () => {
const { server } = makeServer();
const res = await request(server, { method: 'GET', url: '/alpha?token=good-token' });
assert.equal(res.status, 200);
});
test('one noisy endpoint cannot starve the others', async () => {
const { server } = makeServer();
let limited = 0;
for (let i = 0; i < 61; i++) {
const r = await request(server, {
url: '/alpha', headers: auth(SECRET_A), body: post({ message: `n${i}` })
});
if (r.status === 429) limited++;
}
assert.ok(limited > 0, 'the per-endpoint budget must trip before the global one');
const other = await request(server, {
url: '/legacy', headers: auth(SECRET_B), body: post({ message: 'still fine' })
});
assert.equal(other.status, 200, 'a different endpoint keeps its own budget');
});
test('a secretless endpoint is never served', async () => {
const { server } = makeServer();
server.setEndpoints([{ id: 'empty', name: 'Empty', secret: '', schema: SCHEMA }]);
assert.deepEqual(server.endpointIds(), []);
const res = await request(server, {
url: '/empty', headers: { 'x-md-webhook-secret': '' }, body: post({ message: 'hi' })
});
assert.equal(res.status, 401);
});