Skip to content

Proposal G, PR3: opaque-blob saved-decks API - #88

Closed
WilfordGrimley wants to merge 2 commits into
claude/proposal-g-schema-backendfrom
claude/proposal-g-saved-decks-api
Closed

Proposal G, PR3: opaque-blob saved-decks API#88
WilfordGrimley wants to merge 2 commits into
claude/proposal-g-schema-backendfrom
claude/proposal-g-saved-decks-api

Conversation

@WilfordGrimley

Copy link
Copy Markdown

Third of 4 sequenced PRs for docs/proposals/proposal-g-user-accounts-saved-decks.md (schema+backend [#85] → Discord OAuth UX [#86] → this PR → frontend).

Stacked on #85 deliberately (this PR's views/tests import SavedDeck/UserCryptoProfile, which only exist on that branch) — opened as draft for exactly that reason, and marked with a merge-time TODO to retarget to master once #85 merges, per the stacked-PR base-deletion trap documented in docs/lessons.md. Please don't merge this before #85 merges and this gets retargeted.

Description

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; an 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 original 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. 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 — deck ciphertext/wrapped-DEKs are never touched). kdfIterations is checked against SAVED_DECK_MIN_KDF_ITERATIONS (default 600,000, matching §8's own floor).
  • resetSavedDecks requires an explicit confirm: true and deletes every SavedDeck + 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, regenerated schema_types.py/schema_types.ts via quicktype.

A real gotcha caught along the way: adding schemas with a kind enum property caused quicktype's naming/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 before diffing — the committed files are always post-processed, not raw generator output.

Checklist

  • I have installed pre-commit and installed the hooks with pre-commit install before creating any commits.
  • I have updated any related tests for code I modified or added new tests where appropriate.
  • I have manually tested my changes as follows:
    • A 17-assertion smoke script run via Django's test client against real local Postgres: 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 — could not execute it via pytest in this sandbox (same testcontainers/Docker limitation as PR1), but CI has real Docker and already ran PR1's model tests successfully.
    • ruff, isort --check-only, black==22.8.0 --check, mypy --config-file mypy.ini all clean on the backend.
    • tsc --noEmit, next lint, prettier --check all clean; full 304-test jest suite passes on the frontend.
  • I have updated any relevant documentation or created new documentation where appropriate. (API-only PR; no new user/admin-facing behavior to document yet — lands with the frontend PR)

Merge-time checklist


Generated by Claude Code

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.
@WilfordGrimley
WilfordGrimley deleted the branch claude/proposal-g-schema-backend July 18, 2026 22:24
WilfordGrimley added a commit that referenced this pull request Jul 18, 2026
* Proposal G, PR4b: carry forward frontend prerequisites from PR3/PR4a

frontend/src/common/schema_types.ts (regenerated saved-deck types) and
frontend/src/store/api.ts's Kind->VoteQueueRequestKind import fix come
from PR #88 (claude/proposal-g-saved-decks-api); savedDeckCrypto.ts,
its tests, and the jest.setup.ts crypto.subtle polyfill come from PR #89
(claude/proposal-g-crypto-module). Both PRs are still open, so this
branch is based on master directly (not stacked) and carries identical
copies of just the frontend files this UI work needs, to avoid a 3-deep
PR stack. Verified byte-identical against each source branch; tsc
--noEmit and the crypto module's jest suite both pass on this base.

Deviation note for the merge-time checklist: once PR #88 and/or #89
merge to master, this branch's copies of these files will already match
what lands there — rebase onto master before merging PR4b to drop the
now-redundant duplicate commit cleanly (or let git no-op it; content is
identical either way).

* Proposal G, PR4b: carry forward sign-in relocation from PR2, add My Decks nav entry

Mounts AuthWidget in the navbar (relocated off /whatsthat) and adds a
"My Decks" top-level nav entry, gated on an authenticated whoami session,
alongside it. The AuthWidget/Navbar/whatsthat changes are identical to
PR #86 (claude/proposal-g-signin-navbar), carried forward for the same
reason as the prior commit: PR4b needs sign-in visible everywhere for
"My Decks" to be reachable, and #86 is still open. Deliberately did NOT
carry over that branch's stale removal of the Display (beta) nav entry -
that branch predates Proposal H (#87), which added Display; dropping it
here would be a regression, not a carry-forward.

/myDecks route itself doesn't exist yet - added in a following commit.

* Proposal G, PR4b: deck payload/dirty-check plumbing + RTK Query wiring

- features/savedDecks/deckPayload.ts: the plaintext shape encrypted wholesale
  (including its own name), serialize/parse helpers, and deviceLocal
  marking for LocalFile-sourced slots (identifiers are device-specific and
  meaningless elsewhere, so only the flag survives - the card grid's
  existing empty-slot UI becomes the honest re-pick placeholder).
- store/slices/savedDeckSessionSlice.ts: tracks which saved deck (if any)
  the editor represents - session-only, deliberately not wired into
  listenerMiddleware's localStorage persistence.
- projectSlice.loadProject / finishSettingsSlice.loadFinishSettings:
  atomic whole-project replacement, needed for loading a saved deck (no
  existing reducer does this - every other one merges into place).
- features/savedDecks/selectors.ts: selectIsCurrentProjectDirty, per the
  frontend spec's exact definition (differs from last load/save, or is
  non-empty with no prior save at all).
- store/api.ts: the 7 saved-deck/crypto-profile RTK Query endpoints
  (SavedDecks/CryptoProfile cache tags, credentials: "include" + CSRF
  header matching the existing moderation-write convention), with skip
  options so anonymous sessions never fire a doomed authenticated request.

Own-caught fix while wiring the recovery UI: PR4a's recovery-flow test
exercised changePassphrase but never reissued a recovery key, even though
the ZK addendum's recovery flow explicitly re-wraps BOTH slots (passphrase
under the new passphrase, recovery under a FRESH recovery key) once the
old recovery key has actually been used - an ordinary passphrase change
(already covered by a separate test) correctly leaves the recovery slot
alone, but the full recovery path is a distinct case that wasn't covered
in savedDeckCrypto.ts at all. Added rewrapMasterKeyWithNewRecoveryKey and
extended the recovery-flow test to cover the new key end-to-end, including
that the superseded old recovery key no longer unwraps the new slot.

* Proposal G, PR4b: CryptoSessionProvider (in-memory master key context)

A plain React Context (not Redux, since CryptoKey isn't serializable),
mounted in Layout.tsx alongside ClientSearchContextProvider. Exposes
status (anonymous/loading/no-profile/locked/unlocked), the unlocked
master key, and createProfile/unlockWithPassphrase/
recoverAndSetNewPassphrase/lock - wired to the getCryptoProfile/
saveCryptoProfile endpoints added in the previous commit. The master key
never persists anywhere, so it clears itself on every reload; lock()
just does that sooner.

6 tests cover every status transition and the recovery flow's fresh
recovery key end to end (createProfile, wrong/correct passphrase unlock,
recover-and-reissue, lock), using a small harness component in the
absence of any existing renderHook precedent in this codebase's test
suite - matches the established render()+screen+MSW convention instead.

* Proposal G, PR4b: passphrase creation + unlock modals

- RecoveryKeyDisplay: the show-once recovery key step (download/print/copy
  + an explicit "I've saved this" acknowledgement gate before continuing),
  shared by both modals below since both flows end with a fresh recovery
  key to show.
- PassphraseSetupModal: the first-save flow - passphrase + confirm, the
  verbatim-spirit unrecoverability warning, then RecoveryKeyDisplay.
- UnlockModal: the once-per-session unlock prompt, with a "Forgot your
  passphrase?" branch into the recovery flow (paste recovery key + set a
  new passphrase -> reissues a fresh recovery key via
  recoverAndSetNewPassphrase -> RecoveryKeyDisplay again).

Own-caught bug, found via a genuine test failure (not flakiness): status
in cryptoSession.tsx fell through to "anonymous" whenever isAuthenticated
was false - including the instant before the whoami query itself had even
resolved. UnlockModal's tests failed with a misleading "wrong passphrase"
error because they could submit before the crypto profile had loaded,
since nothing signaled that loading state (masterKey != null ? "unlocked"
: cryptoProfileQuery.data == null ? "loading" : ... never entered from the
"anonymous" branch). Fixed by giving whoami's own in-flight state a
distinct "loading" status ahead of the isAuthenticated check, added a
regression test that delays the whoami response and asserts "loading"
appears first, and added an isProfileLoading guard to UnlockModal (belt
and suspenders: both the submit handler and the button's disabled state)
so a real click during that window can never misfire either.

* Proposal G, PR4b: My Decks page

- deckPayload.ts: encryptDeckPayloadForSave (fresh per-save DEK; the
  server has no preference between create/update) and
  decryptSavedDeckSummary (unwrap DEK -> decrypt -> parse), the wire-format
  encrypt/decrypt pair the Save action and this page both need.
- MyDecksPage: lists every saved deck, decrypted client-side once the
  crypto session is unlocked (prompting via UnlockModal automatically
  when locked). Named decks and snapshots render as separate groups.
  "Open in editor" loads the decrypted project/finishSettings into Redux
  and records the current-deck breadcrumb state, then navigates to
  /editor. Per-deck delete (confirm via window.confirm, matching the
  existing moderation-panel convention for destructive actions - no
  dedicated confirm-modal component exists in this codebase to reuse).
  A "Lock" action clears the in-memory master key. Account reset is
  reachable from both the locked AND unlocked states (getSavedDecks is
  fetched independently of decryption) since its entire purpose is
  recovering access when unlock is impossible - gated on an explicit
  second confirming click naming the exact deck count, not a modal.
- Deviation: "Discord-gated" account reset is satisfied by requiring an
  already-authenticated session (the same as every other saved-deck
  action) rather than adding a fresh Discord re-auth redirect - the
  backend's post_reset_saved_decks has no freshness/recency check of its
  own to justify one, so a redirect step would be security theater
  without backend enforcement behind it.
- /myDecks route (frontend/src/pages/myDecks.tsx), matching the nav
  entry already added.

6 tests cover every session state (anonymous, no-profile, locked-then-
unlock, decrypted list grouping), the open-in-editor redux/navigation
wiring, delete-with-confirmation, and the two-click reset gate.

* Proposal G, PR4b: editor wiring - Save action, breadcrumb, load safety flow

- SaveDeckModal: the explicit Save action (name prompt pre-filled from
  the current deck, local-file-slot warning, encrypts and calls
  saveDeck, records the returned key). Assumes the crypto session is
  already unlocked.
- LoadSafetyModal: the loss-proof-by-construction load flow (frontend
  spec §4) - dirty + logged-in always saves a safety copy first, never
  skippable. Offers "Update {name}" vs "Save as new snapshot" when the
  current content is itself an already-saved deck; just an inline-
  renameable snapshot save (no skip option) when it was never saved.
- SavedDeckPanel: the reverse breadcrumb ("Editing: {name}" / "Unsaved
  project") plus the Save button, rendered only when authenticated.
  Clicking Save runs PassphraseSetupModal or UnlockModal first if the
  crypto session isn't ready. Also raises the one-time anonymous->login
  adopt-by-save toast (informational only - the Toasts system has no
  action-button support, and extending shared toast infra for one caller
  wasn't worth it, so it just points at the Save button below).
- Wired SavedDeckPanel into ProjectEditor's action cluster, and
  LoadSafetyModal into MyDecksPage's "Open in editor" (dirty-check via
  selectIsCurrentProjectDirty; empty/clean editors still load
  immediately, no prompt).

15 new tests across the three modals/panel, using a small
status-exposing test harness to reliably wait for the crypto session to
actually unlock before interacting (a bare "field is present" check
isn't a real signal, since these components render their form
regardless of lock state and just no-op an early submit).

* Fix import-sort lint errors caught by a whole-project next lint pass

selectors.ts and api.ts had passed every earlier per-file eslint/prettier
check in this branch's individual commits, but a full-project `next lint`
(not run until now) caught two real simple-import-sort/imports errors -
per-file lint runs don't always agree with a whole-project pass on
import ordering across an entire changed import block. No behavior change.

* Proposal G, PR4b: real-browser Playwright smoke coverage

3 specs verified live in an actual browser (not jsdom - real WebCrypto,
real Next.js routing, real Bootstrap modals): the editor's Save action/
breadcrumb render once signed in, the My Decks nav entry is hidden
anonymously and appears once signed in, and the empty-state message
renders correctly. Ran locally with a temporary executablePath override
for this sandbox's browser-binary version mismatch (never run
playwright install per environment policy); both playwright.config.ts
and tests/global-setup.ts were reverted back to their committed state
before this commit - only the new spec file is included.

---------

Co-authored-by: Claude <noreply@anthropic.com>
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>
WilfordGrimley added a commit that referenced this pull request Jul 19, 2026
…gns, docs (design/docs only) (#99)

* Proposal G spec: PR-6 design for deck portability (design only)

Formalizes what the zero-knowledge, server-unbound crypto design already
implies: export/import of the complete encrypted bundle (no unlock
required for export - it's the same ciphertext the server already
holds), a versioned public format as the actual portability contract, a
standalone decrypt tool as the trust anchor ("if this site vanishes
tomorrow, your decks are still yours"), honest offline-attackability
limits, and an explicit rejection of any server-bound key material.
Nothing built in this commit - the owner's addendum was explicit that
this lands with a later PR-6.

Also updates the doc's stale header status line (still said "BUILDING...
PR1/PR2 opened" from before any of the 5 sequenced PRs had merged) now
that schema+backend (#85), sign-in relocation (#86), the saved-decks API
(#94, recreated after #88's base-deletion auto-close), the crypto module
(#89), and the frontend UI wiring (#93) have all landed on master - and
adds the portability sentence to the legal data-inventory paragraph, per
the addendum's explicit instruction.

* docs: add docs/features/saved-decks.md now that Proposal G has merged

Task-end wiki/docs check (CLAUDE.md): this changed what a USER sees (My
Decks page, editor Save/breadcrumb, navbar sign-in) - all 5 sequenced
PRs (#85, #86, #94, #89, #93) are now merged, so there's real behavior
to document. Covers the zero-knowledge crypto mental model, backend
endpoints/constants, frontend file map, the still-design-only PR-5/PR-6
addenda, and the owner-only Discord-credentials/legal-review pointers.
Added to docs/README.md's flat index.

Wiki note (cloud session, per CLAUDE.md convention): the project's
GitHub wiki itself (a separate, generated-view target from docs/) likely
wants a new "Saved Decks" user-facing page once this feature is visible
in production - flagging here rather than editing it directly, since
that's the documented cloud-session convention.

* Proposal G spec: PR-7 design for art provenance (design only)

Per-slot provenance (driveId, sourceName, sourceType, optional
contentPhash, indexedBy) in a future deckPayload version (bumps
formatVersion per PR-6's own versioning rule), so an un-indexed slot
renders a direct-from-drive thumbnail with a "not in this catalog"
badge + origin link instead of breaking. States the moderation-bypass
rationale explicitly (user's own private data, client-side fetch, never
served/cached by this server) rather than leaving it implicit. XML 2.0
gains three optional, backwards-compatible attributes for third-party
phash->federation-verdict joins. Hard line: provenance never enters the
federation verdict export, which stays conclusions-only.

Addendum clarifications folded in: importing any XML version with
un-indexed drive IDs still leaves those slots viewable via the same
direct-drive rendering (the badge/link only appear with 2.0+
provenance present); web PDF export of un-indexed slots is explicitly
out of scope for PR-7 (v1 answer is view + print-via-desktop-tool
guidance, not a foregone-conclusion export fallback).

Nothing built - per the owner's explicit instruction, this is spec-only.
Also updates docs/features/saved-decks.md's "not yet built" list and
the proposal doc's Future-work/header pointers to include PR-7 alongside
PR-5/PR-6.

* docs/README.md: fix stale Proposal G status + PR-5/6/7 addenda references

The Plans & proposals status table still said HOLD for Proposal G even
though the core build has fully shipped (only the PR-5/6/7 addenda
remain HOLD) - matches proposal-c's existing PARTIAL precedent for the
same shape (some shipped, some still HOLD). Also fixed the
features/saved-decks.md summary bullet, which still said "PR-5/PR-6"
before PR-7 was added.

* Proposal G spec: PR-6 revision/modifiedAt fields + deck roaming note

Two small fields added to PR-6's encrypted-payload envelope: revision
(int, incremented per save) and modifiedAt (timestamp) - private inside
the payload like everything else, bumping formatVersion per PR-6/PR-7's
shared versioning rule. Purpose: makes an export/import round-trip
self-describing (a bundle can be compared against the server's current
copy without any server-side plaintext comparison) and seeds any future
cross-instance sync with conflict-detection for free.

Also adds a "Deck roaming" future-work paragraph after "Deck sharing":
cross-instance blob sync is ZK-compatible in principle (only ciphertext
would travel, never keys) but is a full protocol in its own right
(discovery, consent, conflict surfacing, deletion propagation) -
explicitly out of scope until federation has real peers to sync
between. Manual export/import (PR-6) is the supported path today; the
new revision/modifiedAt fields exist in part to make that manual path
safe without committing to automatic sync's unsolved questions.

Nothing built - design-only, per the owner's instruction.

---------

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants