screentinker/server/lib
ScreenTinker 85febe05c0 Fix a login-page dead end, an enumeration oracle, and three boot/limiter defects
From the regression sweep. The first is a genuine regression against main.

A RATE-LIMITED DISCOVERY PERMANENTLY DEAD-ENDED THE LOGIN PAGE

lookupOrgSso checked that a body PARSED, not that the request succeeded — and a 429
body is valid JSON. So `data.sso` came back undefined, the single sign-on button was
hidden, the password box restored, and the domain recorded as answered: permanently,
for the life of the page. On an SSO-only domain that is the worst outcome available —
the password box then returns 403 and the button the user is told to use is not on the
screen. Discover is 10/min per IP and one person filling in the form costs up to four
calls, so a few colleagues behind one office address is enough. The comment above that
code already claimed to prevent exactly this; it only ever covered the 5xx case.

THE SSO-ONLY REFUSAL WAS AN ACCOUNT-EXISTENCE ORACLE

403 for an address that exists, 401 for one that does not — from an endpoint whose own
lockout returns 401 specifically to avoid that. The DOMAIN check now runs BEFORE the
account lookup, so both answer identically; whether a domain uses single sign-on is
already public through /sso/discover, so it reveals nothing new. The membership-level
refusal is deliberately downgraded to the generic 401, because a distinct answer there
would put the oracle back for exactly the accounts worth enumerating.

Verified: existing and invented addresses at an SSO-only domain both 403; and on an
instance with NO SSO configured, register/login/wrong-password/unknown-address are
201/200/401/401 — the hoisted check does not touch them.

BOOT PREFLIGHT

  - a cold install ran `npm ci --omit=dev` unconditionally, so a first start on a
    developer machine left `npm test` broken: same class of surprise as the prune this
    file already warns about, through the other branch of the same if. Now production-
    only.
  - two servers starting together: the loser died with ENOTEMPTY even though the tree
    was complete by then. It re-checks before failing.
  - the opt-out accepted only '1', unlike every other boolean the server takes.

THE LIMITER FOLD, DONE PROPERLY

Unmatched paths under /api/organizations still minted a bucket each. My first fix was a
catch-all regex — which put every unknown path in ONE bucket WITH the real endpoints,
so flooding nonsense URLs exhausted the limit for /sso-only. That trades a bypass for a
denial of service. Folding is now by explicit shape: known endpoints keep their own
keys, everything else shares a bucket kept apart from all of them.

Verified: 120 unmatched paths give 60/60 (bypass closed), and after that flood
/sso-only, /sso and /sso/:id/test all still answer 401 rather than 429 (no starvation),
while 70 hits on one real endpoint do trip its own limit. The login trailing-slash
bypass stays closed.

1609 tests, three clean runs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bvjey4FNam49MN7ybjcq6A
2026-08-11 10:22:25 -05:00
..
agency-layouts.js feat: agency zone-grant issuance UI + reactive placement card (#73) 2026-06-14 15:12:55 -05:00
agency-targets.js feat(agency): per-token upload folder — auto-created, subtree-confined (#158) (#171) 2026-07-12 21:23:25 -05:00
apk-cache.js Serve a beta APK alongside the stable one, and let a display move between them 2026-07-30 19:12:46 -05:00
apk-signature.js feat(device-owner): tier foundation + QR provisioning + content-expiry & device enhancements (#168) 2026-07-12 19:41:07 -05:00
app-settings.js feat(#146): always-on devices_connected + admin-toggleable /api/status debug block 2026-07-01 18:45:40 -05:00
billing-token.js feat(#146): owner-only CLI to mint billing:read tokens (scripts/mint-billing-token.js) 2026-07-01 21:31:11 -05:00
billing.js feat(#146): usage metering + admin-gated Billable Screens report (contract system-of-record) 2026-07-01 20:45:27 -05:00
bounded-snapshot-store.js fix(widgets): bound the unauthenticated telemetry store, and stop it writing rows 2026-07-26 10:36:09 -05:00
branding.js fix(security): patch quick-win findings from the codebase review 2026-06-08 19:02:19 -05:00
brightsign-package.js autorun.zip must be STORED and opened with roBrightPackage 2026-08-05 11:36:18 -05:00
brightsign-snapshot-queue.js Let a BrightSign host COLLECT its capture request over HTTP 2026-08-07 14:15:06 -05:00
brightsign-update.js Remote screenshots use the framebuffer, and an opted-in tester can move forward 2026-08-05 12:33:34 -05:00
chunked-prune.js fix(#146) A: non-blocking maintenance — chunked+yield+band-gate all sweeps 2026-06-30 20:55:05 -05:00
command-queue.js feat(socket): delivery queue for offline-device emits 2026-05-14 13:06:43 -05:00
content-ack-limiter.js fix(#146) E: log/write self-protection — coalesced logs, batched telemetry, bounded maps 2026-06-30 21:34:01 -05:00
content-ingest.js Probe video asynchronously — the sweep would have blocked the loop per file 2026-08-07 08:42:34 -05:00
device-identity.js fix(#146) P1.1: resolveIdentity short-circuits on device_id (zero-lookup hot path) 2026-06-30 21:57:55 -05:00
device-sanitize.js Scope device serialization to what each endpoint actually needs 2026-07-27 20:40:40 -05:00
device-settings.js Apply a saved device snapshot only inside the workspace it was taken in 2026-07-30 20:56:59 -05:00
device-timezone.js Store a schedule in the timezone its screen runs in 2026-07-28 09:43:46 -05:00
domain-verify.js SSO: refuse delegated proof names, release lapsed and deleted claims 2026-08-10 21:01:26 -05:00
emailVerify.js feat(auth,tizen): TOTP 2FA UI, email verification on signup, Tizen SSSP install 2026-07-22 21:08:50 -05:00
flap-limiter.js fix(#148) Item 1: exempt paired+authenticated devices from the flap-limiter quarantine 2026-07-02 14:59:25 -05:00
ghcr-check.js feat(dashboard): version indicator + GHCR update check (#165) 2026-07-10 22:40:06 -05:00
image-gen.js feat(ai): generate background + foreground images for signs (#41 Phase 2) 2026-06-09 13:40:14 -05:00
incident-classify.js feat(diagnostics): device incident log — offline cause, network-vs-reboot, display-sleep (#175) 2026-07-13 11:26:04 -05:00
item-duration.js Default a video playlist item to the clip's own length (#237) 2026-08-06 09:36:53 -05:00
limiter-telemetry.js Record auth rate-limit rejections so they can be measured 2026-07-28 14:01:00 -05:00
liveness.js QA: close four ways a control or an asset lied about itself 2026-08-06 16:12:29 -05:00
log-coalescer.js fix(#146) P3.7: coalescer carries the PEAK numeric over the window 2026-06-30 22:10:45 -05:00
login-lockout.js feat(auth): bound password login per account, not only per IP 2026-07-26 14:07:16 -05:00
media-mute.js Mute reaches YouTube items — it never did, and failed opposite ways per player 2026-08-05 10:50:59 -05:00
media-orientation.js fix(content+android): rotation-aware media — portrait upright on dashboard AND player (#170) (#172) 2026-07-12 22:05:11 -05:00
media-tools.js Backfill missing thumbnails at boot, and say when ffmpeg is absent 2026-08-07 09:30:32 +01:00
numeric-code.js fix(devices): generate access-gating six-digit codes with a CSPRNG 2026-07-26 14:24:03 -05:00
oidc-providers.js SSO-only: enforce per-domain, cover invited members, and stop the admin locking themselves out 2026-08-11 09:51:40 -05:00
oidc.js SSO: refuse delegated proof names, release lapsed and deleted claims 2026-08-10 21:01:26 -05:00
orientation-style.js Preview a rotated display the way people see it, not the way its framebuffer is 2026-08-06 09:38:31 -05:00
ota-breaker.js Serve a beta APK alongside the stable one, and let a display move between them 2026-07-30 19:12:46 -05:00
ota-download-guard.js feat(#146): /api/status.debug throughput counters (gauges -> gauges + work done) 2026-06-30 23:24:32 -05:00
pair-lockout.js fix(pairing): expire a pairing code on device liveness, not row age 2026-07-27 10:59:38 -05:00
parse-size.js Parse MAX_FILE_SIZE, and document what else caps an upload 2026-07-28 20:40:14 -05:00
passwordReset.js feat(auth): self-service password reset 2026-07-27 11:19:39 -05:00
permissions.js feat(roles): add cross-org platform_operator staff role (#13) 2026-06-05 10:30:21 -05:00
player-cache-policy.js Offline media on every player, and a revision so the cache can still be updated 2026-08-05 15:27:36 -05:00
player-capabilities.js A baseline moves when the fix reaches SCREENS, which is not one rule 2026-08-06 22:54:45 -05:00
player-media-health.js Export shared modules to the browser even when Node is in the page 2026-08-07 10:36:50 -05:00
preflight-deps.js Fix a login-page dead end, an enumeration oracle, and three boot/limiter defects 2026-08-11 10:22:25 -05:00
public-email-domains.js SSO: fix an account takeover, a remote crash, and login CSRF found in review 2026-08-10 18:12:07 -05:00
reconnect-throttle.js fix(#146): reconnect/heartbeat storm containment (beta5) 2026-06-29 22:12:13 -05:00
recovery-grant.js fix(auth): back break-glass recovery with a revocable, auditable grant 2026-07-26 14:49:03 -05:00
rolling-counter.js feat(#146): /api/status.debug throughput counters (gauges -> gauges + work done) 2026-06-30 23:24:32 -05:00
safe-socket.js fix(#146): crash-hardening — one device's handler throw can't take down the fleet 2026-06-29 23:38:11 -05:00
schedule-eval.js Export shared modules to the browser even when Node is in the page 2026-08-07 10:36:50 -05:00
schema-check.js fix(db): observable migrations + fail-fast schema verification (#37) 2026-06-09 09:31:52 -05:00
secretbox.js feat(ai): AI content design in the Designer, BYO endpoint (#41 Phase 1) 2026-06-09 12:23:55 -05:00
session-settle.js fix(#148) patch2: per-device session-settle debounce — absorb duplicate-socket storms 2026-07-02 19:12:46 -05:00
settings-pin.js The settings PIN can be rotated and set from the dashboard 2026-08-05 13:44:15 -05:00
socket-rooms.js feat(socket): Phase 2.3 workspace-scoped dashboard socket rooms + per-command permission gates. Dashboard namespace was previously a flat broadcast - every connected dashboard received every device's status/screenshot/playback events platform-wide (foreign device names + IPs included). Inbound socket commands gated by a legacy admin/superadmin role check that was dead code post-Phase-1 rename. 2026-05-12 11:34:24 -05:00
ssrf-guard.js feat: transition engine — GL wipes across web, Tizen & Android (+ image↔video) (#204) 2026-07-20 16:45:32 -05:00
status-log-writer.js feat(diagnostics): device incident log — offline cause, network-vs-reboot, display-sleep (#175) 2026-07-13 11:26:04 -05:00
sync-backend.js Drop the user-agent fallback — it could never fire 2026-08-05 10:30:10 -05:00
tcp-keepalive.js fix(#148) Items 2-4: mark-offline closes the socket + tighten ping + TCP keepalive 2026-07-02 14:59:25 -05:00
tenancy.js refactor(auth): drop the unused optionalAuth middleware 2026-07-24 21:03:25 -05:00
tenant-cascade-migration.js fix(db): cascade tenant resources on workspace/org delete (#18 follow-up) 2026-06-08 16:01:52 -05:00
thumbnail-backfill.js Backfill missing thumbnails at boot, and say when ffmpeg is absent 2026-08-07 09:30:32 +01:00
totp-lockout.js feat(server): TOTP primitives - encrypted secret, hashed recovery codes, verify lockout (#100) 2026-06-13 20:48:55 -05:00
totp.js feat(auth,tizen): TOTP 2FA UI, email verification on signup, Tizen SSSP install 2026-07-22 21:08:50 -05:00
transition-bundle.js feat: transition engine — GL wipes across web, Tizen & Android (+ image↔video) (#204) 2026-07-20 16:45:32 -05:00
transition-config.js feat: transition engine — GL wipes across web, Tizen & Android (+ image↔video) (#204) 2026-07-20 16:45:32 -05:00
upload-sniff.js fix(uploads): derive stored type from file content, and never serve uploads as documents 2026-07-25 11:47:09 -05:00
user-deletion.js feat(#150): preserve per-device settings across delete+re-pair (fingerprint-keyed) 2026-07-07 12:40:47 -05:00
wall-geometry.js Describe a portrait video wall as portrait, and stop a wall hiding its screens 2026-08-06 09:46:31 -05:00
wgt-cache.js feat(auth,tizen): TOTP 2FA UI, email verification on signup, Tizen SSSP install 2026-07-22 21:08:50 -05:00
zone-validate.js fix: per-item mute round-trip + multi-zone orphan-zone fallback & warnings 2026-06-22 23:16:29 -05:00