screentinker/frontend/js/views
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
..
activity.js Stop eight dashboard views reporting success for requests the server refused 2026-07-30 21:21:56 -05:00
admin-player-debug.js Add player debug overlay and server-side error telemetry sink 2026-05-15 15:20:42 -05:00
admin.js SSO: build the operator approval screen, and close the last of the QA findings 2026-08-11 08:48:53 -05:00
billing.js i18n batch 5: wire layout-editor + video-wall + billing (~85 keys) 2026-04-29 20:13:38 -05:00
content-library.js Stop the content edit dialog rewriting types it cannot represent 2026-07-30 21:09:59 -05:00
dashboard.js QA: close four ways a control or an asset lied about itself 2026-08-06 16:12:29 -05:00
designer.js Translate the labels that never went through t() 2026-07-28 19:52:52 -05:00
device-detail.js Merge branch 'feat/brightsign-ip-from-js' 2026-08-10 15:40:31 -05:00
force-password-change.js feat(admin): admin-provisioned user creation + first-login gate (#10) 2026-06-05 11:03:56 -05:00
help.js Describe a portrait video wall as portrait, and stop a wall hiding its screens 2026-08-06 09:46:31 -05:00
kiosk.js Stop eight dashboard views reporting success for requests the server refused 2026-07-30 21:21:56 -05:00
layout-editor.js Portrait templates, a canvas that matches the layout, and a playlist mockup 2026-08-05 13:23:15 -05:00
login.js Fix a login-page dead end, an enumeration oracle, and three boot/limiter defects 2026-08-11 10:22:25 -05:00
no-workspace.js feat(signup): optional org-on-create for self-service signups (#12) 2026-06-05 11:16:27 -05:00
onboarding.js Default a video playlist item to the clip's own length (#237) 2026-08-06 09:36:53 -05:00
playlists.js Merge #239: let the playlist preview skip to any item 2026-08-06 09:52:20 -05:00
reports.js Stop eight dashboard views reporting success for requests the server refused 2026-07-30 21:21:56 -05:00
schedule.js Stop eight dashboard views reporting success for requests the server refused 2026-07-30 21:21:56 -05:00
settings.js SSO: build the operator approval screen, and close the last of the QA findings 2026-08-11 08:48:53 -05:00
teams.js Stop eight dashboard views reporting success for requests the server refused 2026-07-30 21:21:56 -05:00
video-wall.js QA: close four ways a control or an asset lied about itself 2026-08-06 16:12:29 -05:00
widgets.js Stop eight dashboard views reporting success for requests the server refused 2026-07-30 21:21:56 -05:00
workspace-members.js feat(admin): admin-provisioned user creation + first-login gate (#10) 2026-06-05 11:03:56 -05:00