fix(hik-alarm): listen on all methods + skippable source-IP guard (WSL)

Diagnosed why no camera push ever landed: (1) the route only registered
POST/GET, so a probe with another method got a generic 404 the camera
reads as "service available" while our handler never ran; (2) more
fundamentally, WSL mirrored mode REWRITES the inbound source IP to the
host's own address (10.0.10.203), so the camera's real IP (10.0.10.12)
never survives and the source-IP guard rejected every push as a mismatch.

- Register the event route on POST/GET/PUT/PATCH/DELETE/OPTIONS (HEAD comes
  with GET) so ANYTHING hitting the path reaches the handler and is recorded.
- Log + store the HTTP method of each hit; log every hit on arrival, before
  any guard, so even a rejected probe is visible immediately.
- Add per-device skipSourceIpCheck (a Setup checkbox) to bypass the
  source-IP guard where the network rewrites the source (WSL). Digest auth +
  the signed ledger remain the real guards.

Tests: hik-alarm 10 (skip-IP accept + method capture). server green;
web build green (new checkbox renderer + this field).

Claude-Session: https://claude.ai/code/session_01Xcm6ikLgGoCxxHrxtjkk5V
This commit is contained in:
2026-06-22 11:02:17 +02:00
parent 461275521d
commit b7300ec080
3 changed files with 58 additions and 8 deletions
@@ -138,6 +138,23 @@ describe("Hikvision Alarm Server push", () => {
expect(alarmEvents()[0]!.detail.eventType).toBe("vehicleDetection");
});
it("accepts a push from ANY source IP when skipSourceIpCheck is set (WSL rewrites it)", async () => {
// WSL mirrored mode rewrites the inbound source to the host's own IP, so the camera's
// real IP never survives and a strict check rejects every push. With the opt-out, a
// push from the 'wrong' IP is accepted.
seedHikCamera({ skipSourceIpCheck: true });
const res = await app.inject({
method: "POST",
url: `/api/devices/hikvision/${CAM_ID}/event`,
headers: { "content-type": "application/xml" },
payload: VEHICLE_XML,
remoteAddress: "10.0.10.203", // the rewritten host IP, NOT the camera's
});
expect(res.statusCode).toBe(200);
expect(alarmEvents()).toHaveLength(1);
expect(alarmEvents()[0]!.detail.target).toBe("vehicle");
});
it("rejects a push from a DIFFERENT source IP (404, nothing recorded)", async () => {
seedHikCamera();
const res = await app.inject({