feat(schema): encrypted addon-config collection (issue #20) #2

Merged
richiexec merged 1 commit from feature/addon-config-schema into main 2026-07-18 12:30:15 +00:00
Collaborator

Server half of myanime-app#20 — the addon_config collection that holds a user's addon configuration as a client-side-encrypted blob, so a fresh device sign-in restores the addons without the server ever storing usable credentials.

What's here

  • pb_migrations/1785200000_addon_config.js — one row per profile (unique index), OWNS access rules like the other per-profile collections. Stores blob (AES-GCM ciphertext, opaque), salt (per-account KDF salt — not secret; lets a second device derive the same key from the password), kdf (derivation descriptor), plus the same two-clock model as #11 (updatedAt client clock for LWW, updated autodate as the pull cursor).
  • README.md — data model row + a section on why this one blob is encrypted while the rest stays server-readable (the no-E2E stance is unchanged).
  • scripts/verify.py — schema assertions (fields, unique index, per-owner rules), a ciphertext round-trip that checks the client updatedAt isn't restamped and a duplicate-per-profile is rejected, and cross-user isolation checks for addon_config.

Notes

  • Stacks on the sync-fields PR #1. This branch is based on feature/sync-fields, so until that merges the diff here also shows its commit; it collapses to just the addon_config change once PR #1 lands. Please merge after PR #1.
  • The addon_config migration itself is independent of the sync-fields work — it only needs the profiles collection from the init schema (already on main).

🤖 Generated with Claude Code

Server half of [myanime-app#20](https://projects.petruzalekr.cz/richiexec/myanime-app/issues/20) — the `addon_config` collection that holds a user's addon configuration as a **client-side-encrypted blob**, so a fresh device sign-in restores the addons without the server ever storing usable credentials. ## What's here - **`pb_migrations/1785200000_addon_config.js`** — one row per profile (unique index), `OWNS` access rules like the other per-profile collections. Stores `blob` (AES-GCM ciphertext, opaque), `salt` (per-account KDF salt — not secret; lets a second device derive the same key from the password), `kdf` (derivation descriptor), plus the same two-clock model as #11 (`updatedAt` client clock for LWW, `updated` autodate as the pull cursor). - **`README.md`** — data model row + a section on why this one blob is encrypted while the rest stays server-readable (the no-E2E stance is unchanged). - **`scripts/verify.py`** — schema assertions (fields, unique index, per-owner rules), a ciphertext round-trip that checks the client `updatedAt` isn't restamped and a duplicate-per-profile is rejected, and cross-user isolation checks for `addon_config`. ## Notes - **Stacks on the sync-fields PR #1.** This branch is based on `feature/sync-fields`, so until that merges the diff here also shows its commit; it collapses to just the `addon_config` change once PR #1 lands. Please merge **after** PR #1. - The `addon_config` migration itself is independent of the sync-fields work — it only needs the `profiles` collection from the init schema (already on `main`). 🤖 Generated with [Claude Code](https://claude.com/claude-code)
claude added 1 commit 2026-07-18 01:09:07 +00:00
Adds the addon_config collection so a logged-in user's addon configuration
(TorBox key, Czech-dub creds, adult addon, TMDB key) can follow their account
to a fresh device — encrypted client-side, so the server only ever holds
ciphertext.

- pb_migrations/1785200000_addon_config.js: one row per profile (unique index),
  OWNS access rules like the other per-profile collections. Stores blob
  (AES-GCM ciphertext), salt (per-account KDF salt; not secret), kdf
  (derivation descriptor), plus the same two-clock model as #11 (client
  updatedAt for LWW, server updated as the pull cursor).
- README.md: data model + a section on why this one blob is encrypted.
- scripts/verify.py: schema assertions + ciphertext round-trip + cross-user
  isolation checks for addon_config.

Stacks on the #11 sync-fields backend work (PR #1). Client half is
myanime-app's encrypted addon-config sync PR (issue #20).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Author
Collaborator

Review: approve — deploy before merging client #29

Clean, and it matches the client exactly.

  • The addon_config collection stores ciphertext onlyblob + the non-secret salt/kdf + the two-clock updatedAt (client) / updated (autodate cursor), one row per profile (unique index), owned via profile.user = @request.auth.id on all 5 verbs. That's precisely the shape client PR #29 writes ({profile, blob, salt, kdf, updatedAt}) and the same LWW/cursor model as the #11 collections.
  • Migration is additive (one new collection, cascadeDelete off the profile) with a clean down; timestamp 1785200000 sorts after sync_fields. Safe to apply to the live backend.
  • verify.py coverage is exactly right: existence + per-owner rules on all 5 verbs, field types, the unique-profile index, a client-clock round-trip, duplicate-per-profile rejected (400), and cross-user isolation (bob can't list or view alice's blob).

Field sizes are fine (blob max 100000 ≫ the ~1KB base64 of a small AES-GCM blob; salt/kdf comfortably bounded).

Deploy order: merge + redeploy this, then merge client #29 — not the other way round, or #29's writes 404 silently (the reconcile no-ops without surfacing an error).

## Review: approve ✅ — deploy before merging client #29 Clean, and it matches the client exactly. - The `addon_config` collection stores **ciphertext only** — `blob` + the non-secret `salt`/`kdf` + the two-clock `updatedAt` (client) / `updated` (autodate cursor), one row per profile (unique index), owned via `profile.user = @request.auth.id` on all 5 verbs. That's precisely the shape client PR #29 writes (`{profile, blob, salt, kdf, updatedAt}`) and the same LWW/cursor model as the #11 collections. - Migration is **additive** (one new collection, `cascadeDelete` off the profile) with a clean down; timestamp `1785200000` sorts after `sync_fields`. Safe to apply to the live backend. - `verify.py` coverage is exactly right: existence + per-owner rules on all 5 verbs, field types, the unique-profile index, a **client-clock round-trip**, **duplicate-per-profile rejected (400)**, and cross-user isolation (bob can't list or view alice's blob). Field sizes are fine (`blob` max 100000 ≫ the ~1KB base64 of a small AES-GCM blob; `salt`/`kdf` comfortably bounded). **Deploy order:** merge + redeploy this, *then* merge client #29 — not the other way round, or #29's writes 404 silently (the reconcile no-ops without surfacing an error).
richiexec merged commit ed25627f5f into main 2026-07-18 12:30:15 +00:00
richiexec deleted branch feature/addon-config-schema 2026-07-18 12:30:15 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: richiexec/amber-backend#2
No description provided.