Commit Graph
12 Commits
Author SHA1 Message Date
Admin 0e24c4e4f0 [feat](alarm): let devices subscribe without an account, with a token this service mints
Receiving an alarm should not require signing up, and it should not require
pretending to be a user either: an anonymous subscriber is not an account, it
is a subscription. The row is the identity.

joinAlert now takes NO access token. The subscribe token from the QR is the
authorisation, and the service answers with a deviceToken (32 CSPRNG bytes,
base64url, stored only as SHA-256) returned exactly once. Later subscriber
calls send it as X-Device-Token (?deviceToken= on the WebSocket upgrade, which
cannot set headers): getAlert, alarmStatus, ackAlarm, removeSubscription,
qrImage and GET /ws/alarm.

The credential is deliberately narrow. It names one subscription on one alert,
so an ack addresses the ledger row directly and 'am I subscribed' is a field
comparison. createAlert/triggerAlert/disarmAlert/deleteAlert/listSubscriptions
never see it, so they keep answering 401 for a subscriber without any new gate:
owning an alert still needs an account, and the admin token stays the only
secret that can ring or silence one.

Re-joining from the same device rotates the token (one row per alert+device),
so a lost credential is recoverable with the QR and a leaked one dies on the
next join. ALARM_ANONYMOUS=false puts joinAlert back behind login and stores the
subscription against the account, exactly like rev 9.

Migration 010 is additive and idempotent: subscriber_user_id becomes nullable,
device_token_hash gets a unique index, and alarm_events.actor_user_id admits
NULL because an anonymous join logs an actor-less event.

Verified through Maestro against the dev stack (full table in
planning/platform/anonymous-access/plan.md): join with no token -> 200 and the
auth.users count does not move; getAlert/alarmStatus/ackAlarm with the token;
trigger -> 409 with the frame -> ack acked:1 destroyed:true; WS 101 with both
credentials (403 wrong subscribe token, 401 bad device token); old token 401
after a re-join; admin actions 401.
2026-10-19 18:37:17 +01:00
Admin 914fd6570d [fix](alarm): label the debug compose for the data plane
Maestro resolves a service through the org.dcntrlized.service and org.dcntrlized.port labels; without them the single data plane answers unknown service for the dev stack, which is the only supported client path. Allow AUTH_SERVICE_URL to be overridden so the dev fleet can point at auth-debug.
2026-10-19 02:52:02 +01:00
Admin 3cd04105bb [docs](alarm): record the four alert endings
Document the wire contract as implemented: single-origin access through Maestro, no auto-stop, the owner-set persistence cap, the durable delivery ledger and its 409 check-in, explicit acks, hashed device ids, one anonymised event log, and the four ways an alert ends (delivered, expired, disarmed, deleted). Also corrects the heading to match the table it introduces.
2026-10-19 02:28:59 +01:00
Admin 6873b1a3f1 [chore](alarm): ignore test logs from every source package
Tests run with their package directory as cwd, so any package can drop a logs/ directory. The per-package list kept going stale; match src/**/logs instead.
2026-10-19 02:28:59 +01:00
Admin 55e83d6bfd [feat](alarm): hash client identifiers in stdout request logs
A request log should not name a client directly, and the durable event log deliberately has no network columns. Hash the IP and user agent with internal/loghash for the stdout line; empty values stay empty rather than logging a constant digest. This is record anonymisation, not cryptographic protection, and the package says so.
2026-10-19 02:28:59 +01:00
Admin 028f9e7dae [refactor](alarm): make a readable message structural on every error
An error status with an empty body is a client-breaking bug that review does not reliably catch. Route every JSON and plain-text error through internal/apierr, which substitutes the code canonical message when a caller supplies none, and keep the frozen codes in one package shared by the action and WebSocket layers.
2026-10-19 02:26:46 +01:00
Admin 3ccea3e9f6 [feat](alarm): keep one anonymised lifecycle event log
Record created/joined/triggered/delivered/destroyed with SHA-256 hashes of the alert and user UUIDs only, and a reason on destruction. The earlier analytics and stdout-mirror tables were superseded by the user ruling, so 009 creates alarm.event_log, migrates what still applies and drops both, leaving exactly one durable table with no network identifiers, token, body or query string. Retention is age-based via the sweeper.
2026-10-19 02:26:46 +01:00
Admin 6e6a1e9c8a [feat](alarm): add a durable per-device delivery ledger
pg_notify and the socket fan-out are not durable, so an offline phone used to miss a trigger forever and no-auto-stop would leave it ringing at nobody. Write one ledger row per enabled subscription in the trigger transaction, replay what a subscription still owes on every socket connect, and clear a row only on an explicit ack (live socket ack or ackAlarm); alarmStatus is a read-only check-in that answers 409 ERR_ALARM_TRIGGERED_OFFLINE with the missed frame. Ack is idempotent, and a destroyed alert cascades its debt away so a client is never told it missed an alarm that no longer exists.
2026-10-19 02:26:46 +01:00
Admin 0334d7fe93 [feat](alarm): store device ids as sha256 digests
device_id is a stable pseudonymous idempotency key, not a credential; a database dump should not be a usable list of device identifiers. Hash every caller-supplied value before it reaches the table, keep raw values out of logs and subscription views, and migrate existing rows with the identical SQL encoding.
2026-10-19 02:26:31 +01:00
Admin 3a2da52b7a [refactor](alarm): end alerts by disarm or expiry, not a ring timer
An alert now rings from trigger until the owner disarms it; a subscriber silences only its own device. The owner instead sets a persistence cap (default 24h, hard cap 14 days) and the sweeper destroys expired alerts. Disarm ends an armed alert too, and every ending (delivered, expired, disarmed, deleted) converges on the one destruction path so their event-log reasons cannot drift.
2026-10-19 02:26:31 +01:00
Admin f055700c2d [refactor](alarm): retire subscribe-token rotation
The subscribe token is minted once per alert and lives as long as it does; subscribers are never evicted, so the rotation counter had no reader. Drop token_version from the schema and stop serialising tokenVersion on the wire. The QR payload is now built from alert id + token only, which also makes it instance-relative.
2026-10-19 02:26:31 +01:00
Admin ec38e680d6 init 2026-10-18 22:31:16 +01:00