Proposal G, PR3: opaque-blob saved-decks API (recreated after #88's base-deletion auto-close) - #95
Closed
WilfordGrimley wants to merge 2 commits into
Closed
Proposal G, PR3: opaque-blob saved-decks API (recreated after #88's base-deletion auto-close)#95WilfordGrimley wants to merge 2 commits into
WilfordGrimley wants to merge 2 commits into
Conversation
Per §3/§8: require_authenticated decorator (cardpicker/security.py) plus
7 endpoints - 2/savedDecks/ (list), 2/saveDeck/ (upsert), 2/loadDeck/,
2/deleteDeck/, 2/cryptoProfile/ (GET), 2/saveCryptoProfile/ (upsert),
2/resetSavedDecks/ (the destructive last-resort reset). Every
ciphertext/nonce/wrapped-key field is base64 in transit, opaque to the
backend - stored and returned faithfully, never decrypted or inspected.
- saveDeck upserts by key (null creates, existing key updates in place if
owned else 403); kind defaults to "deck"; a "snapshot" create skips the
SAVED_DECK_MAX_PER_USER cap entirely and prunes the owner's snapshot
rows to the newest SAVED_DECK_SNAPSHOT_RING_SIZE (5, a fixed constant
per decision 7, not a setting) afterwards.
- No server-side name-uniqueness check - can't exist once titles are
encrypted (§8's Consequences). The old spec's renameDeck endpoint is
gone too: renaming is now just a normal saveDeck update-by-key call,
since there's no server-visible name to rename.
- get_saved_decks returns full per-deck ciphertext, not just metadata -
the deck's title lives inside it, so the client must decrypt each row
to render "My Decks"; there's no lighter-weight name to return instead.
Documented as a real, closed-eyes tradeoff, not solved differently,
since §8 enumerates the stored fields as an exhaustive list ("nothing
else").
- saveCryptoProfile is an upsert covering both first-save creation and a
passphrase change (which only ever replaces this one row via
update_or_create - deck ciphertext/wrapped-DEKs are never touched).
kdfIterations is checked against SAVED_DECK_MIN_KDF_ITERATIONS (default
600,000, matching §8's own floor) as a defensive floor against a
buggy/malicious client persisting a weak key derivation.
- resetSavedDecks requires an explicit confirm:true and deletes every
SavedDeck + the crypto profile for the requesting user - the "lost
both keys" last resort from §8's Account reset design. No admin-side
or Discord-derived decryption path exists anywhere in this stack, by
design (§8's "Explicitly rejected").
New JSON schemas under schemas/schemas/endpoints/ for all 7
request/response shapes, regenerated schema_types.py/schema_types.ts via
quicktype. Caught and fixed a real quicktype naming collision: adding
schemas with a "kind" enum property caused quicktype's disambiguation to
rename the pre-existing generic `Kind` type (used only by VoteQueueRequest
before now) to `VoteQueueRequestKind` - fixed the two import sites
(views.py, store/api.ts) that referenced the old name. Also had to
isort+black the raw quicktype Python output and prettier the raw
TypeScript output - the committed files are always post-processed, not
raw generator output, and diffing against the unprocessed version
produces a huge spurious diff.
Verified against real Postgres via Django's test client (not pytest -
same testcontainers/Docker limitation as before): a 17-assertion smoke
script covering every endpoint - anonymous rejection, crypto-profile
creation with the iteration floor enforced, deck create/update-in-place,
list scoping, ownership 403s on load/save/delete across two different
owners, cap enforcement with the friendly message, snapshot-ring pruning
to exactly 5 after creating 8, delete, and the destructive reset flow's
confirm requirement and full cleanup - all passed. Added
test_saved_deck_views.py (pytest, mirrors test_moderation_views.py's
ownership/403 pattern) covering the same ground for real CI to run,
which has actual Docker (as it already did for PR1's model tests).
ruff/isort/black==22.8.0/mypy clean on the backend; tsc/eslint/prettier
clean and the full 304-test jest suite passes on the frontend.
…import differently) Ran the correctly-pinned isort==5.12.0 (matching .pre-commit-config.yaml) locally after CI caught a real drift my newer local isort didn't - it collapses a 3-line wrapped import into one line where newer isort leaves it wrapped. No semantic change, purely the import statement's formatting.
4 tasks
Author
|
Superseded by #94, already merged (c064888) — both PRs independently recovered #88 after its stacked-base-deletion auto-close within the same minute (a concurrent-session collision, not a duplicate request). Diffed this branch against current master: it's a strict subset (zero unique content), safe to close without porting anything. |
5 tasks
WilfordGrimley
added a commit
that referenced
this pull request
Jul 18, 2026
…tions (#100) * Consolidate today's conventions into CLAUDE.md; fix proposal docs CLAUDE.md: - Report-relay: replies must carry the full GitHub blob URL to the pushed report, not just branch+path. - New merge-duty rule: never delete a branch in the same action as merging its PR; precondition `gh pr list --base <branch>` empty before deleting (this is how PR #88 was lost to the stacked-PR base-deletion trap). - New rule: search for an existing recovery before rebuilding a lost/auto-closed PR (two sessions rebuilt #88 in parallel; #95 duplicated #94's already-shipped recovery). docs/proposals/proposal-b-bleed-normalization.md: - Correct decision 4's stale pre-PR-2 persistence note: the shipped mechanism is identifier-keyed localStorage (device-local, mirroring favoritesSlice), not SavedDeck/project-cloud state, matching proposal-g's own §5 description of the same mechanism. - Genericize "Proxxied"/"Steam Deck" design-reference mentions. docs/proposals/proposal-c-context-menu-restyle.md: - Mark Part (b) (solid-color utilitarian restyle) SUPERSEDED by Proposal H, which absorbs the restyle direction. - Genericize a "Proxxied" design-reference mention. docs/proposals/proposal-g-user-accounts-saved-decks.md: - Genericize a "Proxxied" design-reference mention. docs/proposals/proposal-h-unified-display-page.md: - Correct alex-taxiera/proxy-print's license label from MIT to AGPL-3.0 (verified against its actual GitHub license metadata); acoreyj/proxies-at-home remains correctly MIT. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AHfxN9bbWAWHs8rfzVtBWt * Add docs/lessons.md entries for PR #91's two findings - "A value carried verbatim out of its old context can silently stop meaning what it meant": generalizes PR #91's starburst width% (relative to a column width that changed under it) together with PR #78's existing "extracts X verbatim" entry as two instances of the same class. - "Bootswatch Superhero hardcodes some component colors as literal properties, not CSS custom-property references": PR #91's .btn-primary background-color finding — verify computed styles on a live element, not just that a --bs-* custom property resolved correctly. Neither was captured in docs/lessons.md by PR #91 itself (checked: its diff only touched cardPanel.tsx and whatsthat.tsx). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AHfxN9bbWAWHs8rfzVtBWt --------- Co-authored-by: Claude <noreply@anthropic.com>
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.
Description
Recreates #88 (
claude/proposal-g-saved-decks-api), which was auto-closed when its base branch (claude/proposal-g-schema-backend, PR #85) was deleted on squash-merge - exactly the "stacked-PR base-deletion trap"docs/lessons.mddocuments and #88's own description flagged as a risk to retarget before merging. I wasn't fast enough to retarget it before GitHub closed it, so this is a fresh PR with the identical content, rebased onto currentmaster(which now already contains #85's models and #86's navbar changes) viagit rebase --onto origin/master 92fb60a7 ...- replaying only this branch's own 2 commits, not #85's now-redundant ones.Content is unchanged from #88 - verified byte-identical (
frontend/src/common/schema_types.ts,frontend/src/store/api.ts) and identical diff stat (22 files, 1616 insertions, 18 deletions) against the original. Re-verified fresh rather than just trusting the rebase:manage.py checkclean,makemigrations --check --dry-runshows no drift against currentmaster's models.black==22.8.0,isort==5.12.0(pinned versions, matching.pre-commit-config.yaml),mypyall clean.require_authenticateddecorator (cardpicker/security.py) plus 7 endpoints -2/savedDecks/(list),2/saveDeck/(upsert),2/loadDeck/,2/deleteDeck/,2/cryptoProfile/(GET),2/saveCryptoProfile/(upsert),2/resetSavedDecks/(the destructive last-resort reset). Every ciphertext/nonce/wrapped-key field is base64 in transit, opaque to the backend - stored and returned faithfully, never decrypted or inspected.saveDeckupserts by key (nullcreates; an existing key updates in place if owned, else 403).kinddefaults to"deck"; a"snapshot"create skips theSAVED_DECK_MAX_PER_USERcap entirely and prunes the owner's snapshot rows to the newestSAVED_DECK_SNAPSHOT_RING_SIZE(5, a fixed constant per decision 7, not a setting) afterwards.renameDeckendpoint is gone too: renaming is now just a normalsaveDeckupdate-by-key call, since there's no server-visible name to rename.get_saved_decksreturns full per-deck ciphertext, not just metadata - the deck's title lives inside it, so the client must decrypt each row to render "My Decks"; there's no lighter-weight name to return instead. A real, closed-eyes tradeoff, not solved differently, since §8 enumerates the stored fields as an exhaustive list ("nothing else").saveCryptoProfileis an upsert covering both first-save creation and a passphrase change (which only ever replaces this one row - deck ciphertext/wrapped-DEKs are never touched).kdfIterationsis checked againstSAVED_DECK_MIN_KDF_ITERATIONS(default 600,000, matching §8's own floor).resetSavedDecksrequires an explicitconfirm: trueand deletes everySavedDeck+ the crypto profile for the requesting user. No admin-side or Discord-derived decryption path exists anywhere in this stack, by design (§8's "Explicitly rejected").New JSON schemas under
schemas/schemas/endpoints/for all 7 request/response shapes, regeneratedschema_types.py/schema_types.tsvia quicktype.A real gotcha caught along the way: adding schemas with a
kindenum property caused quicktype's naming/disambiguation to rename the pre-existing genericKindtype (used only byVoteQueueRequestbefore now) toVoteQueueRequestKind- fixed the two import sites (views.py,store/api.ts) that referenced the old name.Checklist
pre-commitand installed the hooks withpre-commit installbefore creating any commits.manage.py check,makemigrations --check --dry-run(no drift), and a Django-test-client script exercising all 7 endpoints end-to-end against real local Postgres - all passed.black,isort(pinned versions matching.pre-commit-config.yaml),mypy --config-file mypy.iniall clean.tsc --noEmit,next lint,prettier --checkall clean; full jest suite passes (verified viaclaude/proposal-g-ui-wiring(Proposal G, PR4b: passphrase UX, My Decks page, save/load wiring #93), which already carries an identical copy ofschema_types.ts/api.tsfrom this branch and has its own full green test run).test_saved_deck_views.py(pytest, mirrorstest_moderation_views.py's ownership/403 pattern) for CI's real Docker-backed pytest run to execute - this sandbox still can't run pytest directly (testcontainers/Docker limitation).Merge-time checklist
master(not stacked), so there's no retarget step this time.Generated by Claude Code