feat(#49): scope history deletes to the caller, add PULLMD_ALLOW_SIGNUP - #50
Merged
Conversation
Co-Authored-By: Claude Haiku 4.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KfovDXWcqT7abP6EqFedk5
Co-Authored-By: Claude Haiku 4.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KfovDXWcqT7abP6EqFedk5
The call rode along in 7876db9 and is not needed: createCache already creates the users table, and createUserCmd needs nothing from the migration. Since the CLI defaults to mode 'multi-user' while the server defaults to 'disabled', running list-users on an auth-disabled instance with no bootstrap credentials in the environment made this read-only command crash, or silently bootstrap an admin user and claim every existing conversion. Add a regression test that spawns the CLI with no PULLMD_AUTH_MODE, PULLMD_ADMIN_EMAIL or PULLMD_ADMIN_PASSWORD and asserts the users table stays empty afterward.
isAdminUser() only read dataset.isAdmin, which the auth bootstrap never sets in disabled mode (it returns before that assignment). On every auth-disabled instance - the default, and the operator's own production box - both delete buttons promised a user-scoped delete while the server always performs a global one that also invalidates the /s/:id share link. Replace it with isGlobalDelete(), mirroring the server's isGlobalScope predicate (no auth, disabled mode, or admin all delete globally). Also split the delete-all confirmation into scoped and global variants so an admin sees the wider blast radius.
- test/cache-users.test.js: cover the pruneOld orphan guard (it only
runs pruneOrphanFetches when pruneOld actually removed rows), driven
through put() with an aged entry, per the design doc's requirement
that this test was missing.
- test/integration-auth.test.js: rename the describe block from
"admin-only cache deletion" to "cache deletion scope" - it now also
covers the non-admin success case.
- server.js: rename the local `global` to `isGlobal` in the DELETE
/api/cache/:id handler; it shadowed Node's built-in `global`.
- lib/auth.js: coerce the `allowSignup` override to boolean instead of
passing a non-boolean value straight through `??`.
- test/auth-admin-cli.test.js: resolve the repo root with
fileURLToPath instead of new URL('..', ...).pathname, which breaks
on paths with percent-encoded characters; assert stderr is empty
where it was destructured but unused.
- REVIEW-FINDINGS.md, MIGRATION.md: note that S-2's admin-only fix and
multi-user's unconditional self-signup were both superseded in
3.8.0.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KfovDXWcqT7abP6EqFedk5
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #49.
Two things were asked for: let a logged-in non-admin clear entries from their own history, and make self-registration switchable so a public demo instance can run in
multi-usermode without collecting stranger accounts. Diagnosing the first turned up three more defects in the same code paths, fixed here as well.Delete scope instead of a 403
adminOnlyis gone. Both delete routes now pick a scope rather than rejecting:DELETE /api/cache/:idDELETE /api/cacheuser_fetchesrow. Sharedconversionsrow and/s/:idsurvive, other users unaffecteddisabled/unconfiguredResponses gained
scope: "user" \| "global", and delete-all gainedremoved. Existing status codes are unchanged: 400 on an unparsable id, 404 when nothing was deleted, 401 unauthenticated. The predicateisGlobalScopeis character-for-character the condition the deleted middleware used, so admin semantics are preserved rather than reimplemented.Behaviour change worth a CHANGELOG line: in
single-admin/multi-user, a non-adminDELETE /api/cache*now answers200withscope: "user"instead of403 {"error":"Admin required"}.PULLMD_ALLOW_SIGNUPDefault on, so existing
multi-userinstances are unaffected. Onlyfalse/0/no/offcloses registration, in which case the/signuproutes are not mounted at all (404 for GET and POST, no user creatable), the login page drops its "create an account" link, and/api/configreportssignupOpen: false.createAuthexposesallowSignup(raw) andsignupOpen(effective:mode === 'multi-user' && allowSignup). Only the effective value is read anywhere in production code.This also fixes a live bug:
loginPage()rendered the/signuplink unconditionally, so insingle-adminmode the login page linked to a route that does not exist.create-userin the admin CLInode scripts/admin.js create-user <email>with a password prompt, so "registration closed" is not a state an operator cannot escape.Three defects found along the way
user_fetchesrows.pruneOldruns on everyput()and drops conversions older than 90 days, but never removed the matching fetch rows, andcountForUsercounts without the join thathistoryPageForUseruses. The archive therefore reported more entries than it could return, and the drift was permanent. Cleanup is now explicit SQL, plus a one-time sweep atcreateCache. A real FK withON DELETE CASCADEwas deliberately not used: enablingPRAGMA foreign_keyswould start enforcing constraints across the oauth and session tables too. Measured cost of the sweep: 4 ms on a synthetic 50k/50k database.readPassword()read nothing from non-TTY stdin.node:readline/promises'question()returns a promise and ignores a callback, but the non-TTY fallback passed one. Any piped invocation printed the prompt and exited 0 having done nothing. This affected the pre-existingreset-passwordfor as long as that command has existed. Guarded now by a child-process test.showError()was invisible while the archive view was open.showArchive()puts an inlinedisplay:noneon the shared error element, which outranks the.error.visibleclass rule, and both archive delete buttons live inside that view. Two of the three delete paths would have kept failing silently.Frontend
All three delete paths surface failures through the existing
showError(). The tooltip and the delete-all confirmation state their scope, derived from a predicate mirroring the server's. That flag is deliberately tri-state:'unknown'(/api/meanswered nothing) is treated as a global delete, because claiming "global" for a delete that turns out to be scoped costs nothing while the reverse puts a harmless label on a destructive action.Verification
node --test: 1091 pass / 0 fail (1038 onmain).python3 markitdown-sidecar/test_limits.pyandtest_youtube.py: 4 and 9 passed. These are not covered bynode --test.create-usersmoke-tested end to end: creation via pipe, email trimmed and lowercased, duplicate in different case rejected, short password rejected,reset-passwordworking via pipe, both accounts authenticating afterwards.package.jsonis untouched; the version bump belongs to the release. Backward compatible for anyone setting none of the new variables, so this is a minor.Deliberately deferred
db.transactionconstruction placement in two admin-frequency cache methods, an idempotence test for the orphan sweep,!authcoverage for the delete-all route, and a symmetric override truth table. None affect behaviour; each was triaged as ship-as-is.🤖 Generated with Claude Code
https://claude.ai/code/session_01KfovDXWcqT7abP6EqFedk5