feat(derived): encrypt flashcards, study guides, room summaries (#518) - #528

Merged
AndresL230 merged 5 commits into
mainfrom
feat/522-d-518-derived
Aug 6, 2026
Merged

feat(derived): encrypt flashcards, study guides, room summaries (#518)#528
AndresL230 merged 5 commits into
mainfrom
feat/522-d-518-derived

Conversation

@AndresL230

Copy link
Copy Markdown
Collaborator

Closes#518.

Derived content no longer leaks its encrypted sources:

  • flashcards.front/back — both insert paths (generated + imported); dedupe decrypts before Levenshtein
  • study_guides.content — encrypt_json; ETags unaffected (id+generated_at)
  • room_summaries.summary — cache keys on member_hash, unaffected
  • rollout-safe legacy-row reads (tested), backfill runners, encrypted seed, roundtrip tests, oracle entries

Stacked on #521's PR.

🤖 Generated with Claude Code

@coderabbitai

coderabbitaiBot commented Aug 6, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@AndresL230, you've reached your PR review limit, so we couldn't start this review.

Next review available in:53 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 388a06f1-7606-49ec-9f31-c6d9bf3b1f28

📥 Commits

Reviewing files that changed from the base of the PR and between c19a880 and 6bf1b18.

📒 Files selected for processing (13)
  • CLAUDE.md
  • backend/db/backfill_encryption.py
  • backend/db/seed_local_rich.py
  • backend/e2e_oracles/gather.py
  • backend/routes/flashcards.py
  • backend/routes/study_guide.py
  • backend/services/flashcard_import_service.py
  • backend/services/social_cache_service.py
  • backend/tests/integration/test_encryption_roundtrip.py
  • backend/tests/test_flashcard_import_service.py
  • backend/tests/test_flashcards_routes.py
  • backend/tests/test_social_cache_service.py
  • backend/tests/test_study_guide_encryption.py

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@supabase

supabaseBot commented Aug 6, 2026

Copy link
Copy Markdown

This pull request has been ignored for the connected project ybgqdonkoqftwrmweuyv because there are no changes detected in supabase directory. You can change this behaviour in Project Integrations Settings ↗︎.


Preview Branches by Supabase.
Learn more about Supabase Branching ↗︎.

@cloudflare-workers-and-pages

cloudflare-workers-and-pagesBot commented Aug 6, 2026

Copy link
Copy Markdown

Deploying with Cloudflare Workers Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

StatusNameLatest CommitPreview URLUpdated (UTC)
✅ Deployment successful!
View logs
frontend-staging6bf1b18Commit Preview URL

Branch Preview URL
Aug 06 2026, 01:36 AM

@AndresL230

Copy link
Copy Markdown
CollaboratorAuthor

Code review

No issues found. Checked for bugs and CLAUDE.md compliance: every encrypt/decrypt boundary traced (both flashcard insert paths return plaintext to the client; study-guide ETags stay on id+generated_at; room-summary cache keys on plaintext member_hash), dedupe decrypts before Levenshtein, legacy plaintext rows tolerated on every read path, and the seed/backfill/oracle/roundtrip artifacts carry matching column sets.

🤖 Generated with Claude Code

@AndresL230
AndresL230force-pushed the feat/522-d-518-derived branch 2 times, most recently from e167d28 to add403bCompareAugust 6, 2026 01:22
@AndresL230
AndresL230force-pushed the feat/522-c-521-quiz branch 2 times, most recently from b09c0e8 to 3e78b3eCompareAugust 6, 2026 01:25
@AndresL230
AndresL230force-pushed the feat/522-d-518-derived branch from add403b to 80a5a17CompareAugust 6, 2026 01:25
@AndresL230
AndresL230 changed the base branch from feat/522-c-521-quiz to mainAugust 6, 2026 01:28
AndresL230and others added 5 commits August 5, 2026 21:28
)
Both flashcards insert row-builders (AI-generated /generate, imported
/import/commit) now encrypt front/back before the row reaches Supabase.
The list read (GET /user/{user_id}) decrypts front/back for every
returned row, tolerating legacy plaintext rows via decrypt_if_present's
raw-value fallback. dedup_against_existing decrypts each existing row's
front before normalizing, so dedupe still matches against ciphertext
(and legacy plaintext) rows.
/generate's response is built from a decrypted copy of the inserted
rows (mirroring notes_service's write/read split) so the frontend still
sees plaintext front/back for cards it just generated, instead of the
ciphertext written to the row-builder.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Task 10 of the encryption-coverage epic: study_guides.content is now
ciphertext at write (encrypt_json) and decrypted at both readers
(get_cached_guides' list loop and the /guide cached-row return) via
decrypt_json_column, which also tolerates legacy plaintext dict rows.
The ETag on /cached still derives from id+generated_at only, computed
before decrypt.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…t encryption (#518)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…hed list (#518)
get_cached_guides ran decrypt_json_column unguarded per row — one
corrupt study_guides.content 500ed the WHOLE list instead of degrading
just that entry. Wrap the per-row decrypt in try/except, log a warning,
and fall back to content={} (exam_title/overview blank for that row
only) so the rest of the list still renders.
Extends tests/test_study_guide_encryption.py with a mixed-row case: one
good encrypted guide + one corrupt-content guide returns 200 with the
good guide's exam_title intact and the corrupt one's exam_title == ""
(verified to fail via stash/run/pop when the guard is reverted).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@AndresL230
AndresL230force-pushed the feat/522-d-518-derived branch from 80a5a17 to 6bf1b18CompareAugust 6, 2026 01:28
@AndresL230
AndresL230 merged commit 458ddb2 into mainAug 6, 2026
6 checks passed
Sign up for freeto 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.

Derived content is stored in plaintext while its encrypted source is protected (flashcards, study guides, room summaries)

1 participant

@AndresL230
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

feat(derived): encrypt flashcards, study guides, room summaries (#518) - #528

Merged
AndresL230 merged 5 commits into
mainfrom
feat/522-d-518-derived
Aug 6, 2026
Merged

feat(derived): encrypt flashcards, study guides, room summaries (#518)#528
AndresL230 merged 5 commits into
mainfrom
feat/522-d-518-derived

Conversation

@AndresL230

Copy link
Copy Markdown
Collaborator

Closes#518.

Derived content no longer leaks its encrypted sources:

  • flashcards.front/back — both insert paths (generated + imported); dedupe decrypts before Levenshtein
  • study_guides.content — encrypt_json; ETags unaffected (id+generated_at)
  • room_summaries.summary — cache keys on member_hash, unaffected
  • rollout-safe legacy-row reads (tested), backfill runners, encrypted seed, roundtrip tests, oracle entries

Stacked on #521's PR.

🤖 Generated with Claude Code

@coderabbitai

coderabbitaiBot commented Aug 6, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@AndresL230, you've reached your PR review limit, so we couldn't start this review.

Next review available in:53 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 388a06f1-7606-49ec-9f31-c6d9bf3b1f28

📥 Commits

Reviewing files that changed from the base of the PR and between c19a880 and 6bf1b18.

📒 Files selected for processing (13)
  • CLAUDE.md
  • backend/db/backfill_encryption.py
  • backend/db/seed_local_rich.py
  • backend/e2e_oracles/gather.py
  • backend/routes/flashcards.py
  • backend/routes/study_guide.py
  • backend/services/flashcard_import_service.py
  • backend/services/social_cache_service.py
  • backend/tests/integration/test_encryption_roundtrip.py
  • backend/tests/test_flashcard_import_service.py
  • backend/tests/test_flashcards_routes.py
  • backend/tests/test_social_cache_service.py
  • backend/tests/test_study_guide_encryption.py

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@supabase

supabaseBot commented Aug 6, 2026

Copy link
Copy Markdown

This pull request has been ignored for the connected project ybgqdonkoqftwrmweuyv because there are no changes detected in supabase directory. You can change this behaviour in Project Integrations Settings ↗︎.


Preview Branches by Supabase.
Learn more about Supabase Branching ↗︎.

@cloudflare-workers-and-pages

cloudflare-workers-and-pagesBot commented Aug 6, 2026

Copy link
Copy Markdown

Deploying with Cloudflare Workers Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

StatusNameLatest CommitPreview URLUpdated (UTC)
✅ Deployment successful!
View logs
frontend-staging6bf1b18Commit Preview URL

Branch Preview URL
Aug 06 2026, 01:36 AM

@AndresL230

Copy link
Copy Markdown
CollaboratorAuthor

Code review

No issues found. Checked for bugs and CLAUDE.md compliance: every encrypt/decrypt boundary traced (both flashcard insert paths return plaintext to the client; study-guide ETags stay on id+generated_at; room-summary cache keys on plaintext member_hash), dedupe decrypts before Levenshtein, legacy plaintext rows tolerated on every read path, and the seed/backfill/oracle/roundtrip artifacts carry matching column sets.

🤖 Generated with Claude Code

@AndresL230
AndresL230force-pushed the feat/522-d-518-derived branch 2 times, most recently from e167d28 to add403bCompareAugust 6, 2026 01:22
@AndresL230
AndresL230force-pushed the feat/522-c-521-quiz branch 2 times, most recently from b09c0e8 to 3e78b3eCompareAugust 6, 2026 01:25
@AndresL230
AndresL230force-pushed the feat/522-d-518-derived branch from add403b to 80a5a17CompareAugust 6, 2026 01:25
@AndresL230
AndresL230 changed the base branch from feat/522-c-521-quiz to mainAugust 6, 2026 01:28
AndresL230and others added 5 commits August 5, 2026 21:28
)
Both flashcards insert row-builders (AI-generated /generate, imported
/import/commit) now encrypt front/back before the row reaches Supabase.
The list read (GET /user/{user_id}) decrypts front/back for every
returned row, tolerating legacy plaintext rows via decrypt_if_present's
raw-value fallback. dedup_against_existing decrypts each existing row's
front before normalizing, so dedupe still matches against ciphertext
(and legacy plaintext) rows.
/generate's response is built from a decrypted copy of the inserted
rows (mirroring notes_service's write/read split) so the frontend still
sees plaintext front/back for cards it just generated, instead of the
ciphertext written to the row-builder.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Task 10 of the encryption-coverage epic: study_guides.content is now
ciphertext at write (encrypt_json) and decrypted at both readers
(get_cached_guides' list loop and the /guide cached-row return) via
decrypt_json_column, which also tolerates legacy plaintext dict rows.
The ETag on /cached still derives from id+generated_at only, computed
before decrypt.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…t encryption (#518)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…hed list (#518)
get_cached_guides ran decrypt_json_column unguarded per row — one
corrupt study_guides.content 500ed the WHOLE list instead of degrading
just that entry. Wrap the per-row decrypt in try/except, log a warning,
and fall back to content={} (exam_title/overview blank for that row
only) so the rest of the list still renders.
Extends tests/test_study_guide_encryption.py with a mixed-row case: one
good encrypted guide + one corrupt-content guide returns 200 with the
good guide's exam_title intact and the corrupt one's exam_title == ""
(verified to fail via stash/run/pop when the guard is reverted).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@AndresL230
AndresL230force-pushed the feat/522-d-518-derived branch from 80a5a17 to 6bf1b18CompareAugust 6, 2026 01:28
@AndresL230
AndresL230 merged commit 458ddb2 into mainAug 6, 2026
6 checks passed
Sign up for freeto 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.

Derived content is stored in plaintext while its encrypted source is protected (flashcards, study guides, room summaries)

1 participant

@AndresL230
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(derived): encrypt flashcards, study guides, room summaries (#518) - #528

Merged
AndresL230 merged 5 commits into
mainfrom
feat/522-d-518-derived
Aug 6, 2026
Merged

feat(derived): encrypt flashcards, study guides, room summaries (#518)#528
AndresL230 merged 5 commits into
mainfrom
feat/522-d-518-derived

Conversation

@AndresL230

Copy link
Copy Markdown
Collaborator

Closes#518.

Derived content no longer leaks its encrypted sources:

  • flashcards.front/back — both insert paths (generated + imported); dedupe decrypts before Levenshtein
  • study_guides.content — encrypt_json; ETags unaffected (id+generated_at)
  • room_summaries.summary — cache keys on member_hash, unaffected
  • rollout-safe legacy-row reads (tested), backfill runners, encrypted seed, roundtrip tests, oracle entries

Stacked on #521's PR.

🤖 Generated with Claude Code

@coderabbitai

coderabbitaiBot commented Aug 6, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@AndresL230, you've reached your PR review limit, so we couldn't start this review.

Next review available in:53 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 388a06f1-7606-49ec-9f31-c6d9bf3b1f28

📥 Commits

Reviewing files that changed from the base of the PR and between c19a880 and 6bf1b18.

📒 Files selected for processing (13)
  • CLAUDE.md
  • backend/db/backfill_encryption.py
  • backend/db/seed_local_rich.py
  • backend/e2e_oracles/gather.py
  • backend/routes/flashcards.py
  • backend/routes/study_guide.py
  • backend/services/flashcard_import_service.py
  • backend/services/social_cache_service.py
  • backend/tests/integration/test_encryption_roundtrip.py
  • backend/tests/test_flashcard_import_service.py
  • backend/tests/test_flashcards_routes.py
  • backend/tests/test_social_cache_service.py
  • backend/tests/test_study_guide_encryption.py

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@supabase

supabaseBot commented Aug 6, 2026

Copy link
Copy Markdown

This pull request has been ignored for the connected project ybgqdonkoqftwrmweuyv because there are no changes detected in supabase directory. You can change this behaviour in Project Integrations Settings ↗︎.


Preview Branches by Supabase.
Learn more about Supabase Branching ↗︎.

@cloudflare-workers-and-pages

cloudflare-workers-and-pagesBot commented Aug 6, 2026

Copy link
Copy Markdown

Deploying with Cloudflare Workers Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

StatusNameLatest CommitPreview URLUpdated (UTC)
✅ Deployment successful!
View logs
frontend-staging6bf1b18Commit Preview URL

Branch Preview URL
Aug 06 2026, 01:36 AM

@AndresL230

Copy link
Copy Markdown
CollaboratorAuthor

Code review

No issues found. Checked for bugs and CLAUDE.md compliance: every encrypt/decrypt boundary traced (both flashcard insert paths return plaintext to the client; study-guide ETags stay on id+generated_at; room-summary cache keys on plaintext member_hash), dedupe decrypts before Levenshtein, legacy plaintext rows tolerated on every read path, and the seed/backfill/oracle/roundtrip artifacts carry matching column sets.

🤖 Generated with Claude Code

@AndresL230
AndresL230force-pushed the feat/522-d-518-derived branch 2 times, most recently from e167d28 to add403bCompareAugust 6, 2026 01:22
@AndresL230
AndresL230force-pushed the feat/522-c-521-quiz branch 2 times, most recently from b09c0e8 to 3e78b3eCompareAugust 6, 2026 01:25
@AndresL230
AndresL230force-pushed the feat/522-d-518-derived branch from add403b to 80a5a17CompareAugust 6, 2026 01:25
@AndresL230
AndresL230 changed the base branch from feat/522-c-521-quiz to mainAugust 6, 2026 01:28
AndresL230and others added 5 commits August 5, 2026 21:28
)
Both flashcards insert row-builders (AI-generated /generate, imported
/import/commit) now encrypt front/back before the row reaches Supabase.
The list read (GET /user/{user_id}) decrypts front/back for every
returned row, tolerating legacy plaintext rows via decrypt_if_present's
raw-value fallback. dedup_against_existing decrypts each existing row's
front before normalizing, so dedupe still matches against ciphertext
(and legacy plaintext) rows.
/generate's response is built from a decrypted copy of the inserted
rows (mirroring notes_service's write/read split) so the frontend still
sees plaintext front/back for cards it just generated, instead of the
ciphertext written to the row-builder.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Task 10 of the encryption-coverage epic: study_guides.content is now
ciphertext at write (encrypt_json) and decrypted at both readers
(get_cached_guides' list loop and the /guide cached-row return) via
decrypt_json_column, which also tolerates legacy plaintext dict rows.
The ETag on /cached still derives from id+generated_at only, computed
before decrypt.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…t encryption (#518)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…hed list (#518)
get_cached_guides ran decrypt_json_column unguarded per row — one
corrupt study_guides.content 500ed the WHOLE list instead of degrading
just that entry. Wrap the per-row decrypt in try/except, log a warning,
and fall back to content={} (exam_title/overview blank for that row
only) so the rest of the list still renders.
Extends tests/test_study_guide_encryption.py with a mixed-row case: one
good encrypted guide + one corrupt-content guide returns 200 with the
good guide's exam_title intact and the corrupt one's exam_title == ""
(verified to fail via stash/run/pop when the guard is reverted).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@AndresL230
AndresL230force-pushed the feat/522-d-518-derived branch from 80a5a17 to 6bf1b18CompareAugust 6, 2026 01:28
@AndresL230
AndresL230 merged commit 458ddb2 into mainAug 6, 2026
6 checks passed
Sign up for freeto 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.

Derived content is stored in plaintext while its encrypted source is protected (flashcards, study guides, room summaries)

1 participant

@AndresL230
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(derived): encrypt flashcards, study guides, room summaries (#518) - #528

Merged
AndresL230 merged 5 commits into
mainfrom
feat/522-d-518-derived
Aug 6, 2026
Merged

feat(derived): encrypt flashcards, study guides, room summaries (#518)#528
AndresL230 merged 5 commits into
mainfrom
feat/522-d-518-derived

Conversation

@AndresL230

Copy link
Copy Markdown
Collaborator

Closes#518.

Derived content no longer leaks its encrypted sources:

  • flashcards.front/back — both insert paths (generated + imported); dedupe decrypts before Levenshtein
  • study_guides.content — encrypt_json; ETags unaffected (id+generated_at)
  • room_summaries.summary — cache keys on member_hash, unaffected
  • rollout-safe legacy-row reads (tested), backfill runners, encrypted seed, roundtrip tests, oracle entries

Stacked on #521's PR.

🤖 Generated with Claude Code

@coderabbitai

coderabbitaiBot commented Aug 6, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@AndresL230, you've reached your PR review limit, so we couldn't start this review.

Next review available in:53 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 388a06f1-7606-49ec-9f31-c6d9bf3b1f28

📥 Commits

Reviewing files that changed from the base of the PR and between c19a880 and 6bf1b18.

📒 Files selected for processing (13)
  • CLAUDE.md
  • backend/db/backfill_encryption.py
  • backend/db/seed_local_rich.py
  • backend/e2e_oracles/gather.py
  • backend/routes/flashcards.py
  • backend/routes/study_guide.py
  • backend/services/flashcard_import_service.py
  • backend/services/social_cache_service.py
  • backend/tests/integration/test_encryption_roundtrip.py
  • backend/tests/test_flashcard_import_service.py
  • backend/tests/test_flashcards_routes.py
  • backend/tests/test_social_cache_service.py
  • backend/tests/test_study_guide_encryption.py

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@supabase

supabaseBot commented Aug 6, 2026

Copy link
Copy Markdown

This pull request has been ignored for the connected project ybgqdonkoqftwrmweuyv because there are no changes detected in supabase directory. You can change this behaviour in Project Integrations Settings ↗︎.


Preview Branches by Supabase.
Learn more about Supabase Branching ↗︎.

@cloudflare-workers-and-pages

cloudflare-workers-and-pagesBot commented Aug 6, 2026

Copy link
Copy Markdown

Deploying with Cloudflare Workers Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

StatusNameLatest CommitPreview URLUpdated (UTC)
✅ Deployment successful!
View logs
frontend-staging6bf1b18Commit Preview URL

Branch Preview URL
Aug 06 2026, 01:36 AM

@AndresL230

Copy link
Copy Markdown
CollaboratorAuthor

Code review

No issues found. Checked for bugs and CLAUDE.md compliance: every encrypt/decrypt boundary traced (both flashcard insert paths return plaintext to the client; study-guide ETags stay on id+generated_at; room-summary cache keys on plaintext member_hash), dedupe decrypts before Levenshtein, legacy plaintext rows tolerated on every read path, and the seed/backfill/oracle/roundtrip artifacts carry matching column sets.

🤖 Generated with Claude Code

@AndresL230
AndresL230force-pushed the feat/522-d-518-derived branch 2 times, most recently from e167d28 to add403bCompareAugust 6, 2026 01:22
@AndresL230
AndresL230force-pushed the feat/522-c-521-quiz branch 2 times, most recently from b09c0e8 to 3e78b3eCompareAugust 6, 2026 01:25
@AndresL230
AndresL230force-pushed the feat/522-d-518-derived branch from add403b to 80a5a17CompareAugust 6, 2026 01:25
@AndresL230
AndresL230 changed the base branch from feat/522-c-521-quiz to mainAugust 6, 2026 01:28
AndresL230and others added 5 commits August 5, 2026 21:28
)
Both flashcards insert row-builders (AI-generated /generate, imported
/import/commit) now encrypt front/back before the row reaches Supabase.
The list read (GET /user/{user_id}) decrypts front/back for every
returned row, tolerating legacy plaintext rows via decrypt_if_present's
raw-value fallback. dedup_against_existing decrypts each existing row's
front before normalizing, so dedupe still matches against ciphertext
(and legacy plaintext) rows.
/generate's response is built from a decrypted copy of the inserted
rows (mirroring notes_service's write/read split) so the frontend still
sees plaintext front/back for cards it just generated, instead of the
ciphertext written to the row-builder.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Task 10 of the encryption-coverage epic: study_guides.content is now
ciphertext at write (encrypt_json) and decrypted at both readers
(get_cached_guides' list loop and the /guide cached-row return) via
decrypt_json_column, which also tolerates legacy plaintext dict rows.
The ETag on /cached still derives from id+generated_at only, computed
before decrypt.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…t encryption (#518)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…hed list (#518)
get_cached_guides ran decrypt_json_column unguarded per row — one
corrupt study_guides.content 500ed the WHOLE list instead of degrading
just that entry. Wrap the per-row decrypt in try/except, log a warning,
and fall back to content={} (exam_title/overview blank for that row
only) so the rest of the list still renders.
Extends tests/test_study_guide_encryption.py with a mixed-row case: one
good encrypted guide + one corrupt-content guide returns 200 with the
good guide's exam_title intact and the corrupt one's exam_title == ""
(verified to fail via stash/run/pop when the guard is reverted).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@AndresL230
AndresL230force-pushed the feat/522-d-518-derived branch from 80a5a17 to 6bf1b18CompareAugust 6, 2026 01:28
@AndresL230
AndresL230 merged commit 458ddb2 into mainAug 6, 2026
6 checks passed
Sign up for freeto 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.

Derived content is stored in plaintext while its encrypted source is protected (flashcards, study guides, room summaries)

1 participant

@AndresL230
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

feat(derived): encrypt flashcards, study guides, room summaries (#518) - #528

Merged
AndresL230 merged 5 commits into
mainfrom
feat/522-d-518-derived
Aug 6, 2026
Merged

feat(derived): encrypt flashcards, study guides, room summaries (#518)#528
AndresL230 merged 5 commits into
mainfrom
feat/522-d-518-derived

Conversation

@AndresL230

Copy link
Copy Markdown
Collaborator

Closes#518.

Derived content no longer leaks its encrypted sources:

  • flashcards.front/back — both insert paths (generated + imported); dedupe decrypts before Levenshtein
  • study_guides.content — encrypt_json; ETags unaffected (id+generated_at)
  • room_summaries.summary — cache keys on member_hash, unaffected
  • rollout-safe legacy-row reads (tested), backfill runners, encrypted seed, roundtrip tests, oracle entries

Stacked on #521's PR.

🤖 Generated with Claude Code

@coderabbitai

coderabbitaiBot commented Aug 6, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@AndresL230, you've reached your PR review limit, so we couldn't start this review.

Next review available in:53 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 388a06f1-7606-49ec-9f31-c6d9bf3b1f28

📥 Commits

Reviewing files that changed from the base of the PR and between c19a880 and 6bf1b18.

📒 Files selected for processing (13)
  • CLAUDE.md
  • backend/db/backfill_encryption.py
  • backend/db/seed_local_rich.py
  • backend/e2e_oracles/gather.py
  • backend/routes/flashcards.py
  • backend/routes/study_guide.py
  • backend/services/flashcard_import_service.py
  • backend/services/social_cache_service.py
  • backend/tests/integration/test_encryption_roundtrip.py
  • backend/tests/test_flashcard_import_service.py
  • backend/tests/test_flashcards_routes.py
  • backend/tests/test_social_cache_service.py
  • backend/tests/test_study_guide_encryption.py

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@supabase

supabaseBot commented Aug 6, 2026

Copy link
Copy Markdown

This pull request has been ignored for the connected project ybgqdonkoqftwrmweuyv because there are no changes detected in supabase directory. You can change this behaviour in Project Integrations Settings ↗︎.


Preview Branches by Supabase.
Learn more about Supabase Branching ↗︎.

@cloudflare-workers-and-pages

cloudflare-workers-and-pagesBot commented Aug 6, 2026

Copy link
Copy Markdown

Deploying with Cloudflare Workers Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

StatusNameLatest CommitPreview URLUpdated (UTC)
✅ Deployment successful!
View logs
frontend-staging6bf1b18Commit Preview URL

Branch Preview URL
Aug 06 2026, 01:36 AM

@AndresL230

Copy link
Copy Markdown
CollaboratorAuthor

Code review

No issues found. Checked for bugs and CLAUDE.md compliance: every encrypt/decrypt boundary traced (both flashcard insert paths return plaintext to the client; study-guide ETags stay on id+generated_at; room-summary cache keys on plaintext member_hash), dedupe decrypts before Levenshtein, legacy plaintext rows tolerated on every read path, and the seed/backfill/oracle/roundtrip artifacts carry matching column sets.

🤖 Generated with Claude Code

@AndresL230
AndresL230force-pushed the feat/522-d-518-derived branch 2 times, most recently from e167d28 to add403bCompareAugust 6, 2026 01:22
@AndresL230
AndresL230force-pushed the feat/522-c-521-quiz branch 2 times, most recently from b09c0e8 to 3e78b3eCompareAugust 6, 2026 01:25
@AndresL230
AndresL230force-pushed the feat/522-d-518-derived branch from add403b to 80a5a17CompareAugust 6, 2026 01:25
@AndresL230
AndresL230 changed the base branch from feat/522-c-521-quiz to mainAugust 6, 2026 01:28
AndresL230and others added 5 commits August 5, 2026 21:28
)
Both flashcards insert row-builders (AI-generated /generate, imported
/import/commit) now encrypt front/back before the row reaches Supabase.
The list read (GET /user/{user_id}) decrypts front/back for every
returned row, tolerating legacy plaintext rows via decrypt_if_present's
raw-value fallback. dedup_against_existing decrypts each existing row's
front before normalizing, so dedupe still matches against ciphertext
(and legacy plaintext) rows.
/generate's response is built from a decrypted copy of the inserted
rows (mirroring notes_service's write/read split) so the frontend still
sees plaintext front/back for cards it just generated, instead of the
ciphertext written to the row-builder.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Task 10 of the encryption-coverage epic: study_guides.content is now
ciphertext at write (encrypt_json) and decrypted at both readers
(get_cached_guides' list loop and the /guide cached-row return) via
decrypt_json_column, which also tolerates legacy plaintext dict rows.
The ETag on /cached still derives from id+generated_at only, computed
before decrypt.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…t encryption (#518)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…hed list (#518)
get_cached_guides ran decrypt_json_column unguarded per row — one
corrupt study_guides.content 500ed the WHOLE list instead of degrading
just that entry. Wrap the per-row decrypt in try/except, log a warning,
and fall back to content={} (exam_title/overview blank for that row
only) so the rest of the list still renders.
Extends tests/test_study_guide_encryption.py with a mixed-row case: one
good encrypted guide + one corrupt-content guide returns 200 with the
good guide's exam_title intact and the corrupt one's exam_title == ""
(verified to fail via stash/run/pop when the guard is reverted).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@AndresL230
AndresL230force-pushed the feat/522-d-518-derived branch from 80a5a17 to 6bf1b18CompareAugust 6, 2026 01:28
@AndresL230
AndresL230 merged commit 458ddb2 into mainAug 6, 2026
6 checks passed
Sign up for freeto 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.

Derived content is stored in plaintext while its encrypted source is protected (flashcards, study guides, room summaries)

1 participant

@AndresL230
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(derived): encrypt flashcards, study guides, room summaries (#518) - #528

Merged
AndresL230 merged 5 commits into
mainfrom
feat/522-d-518-derived
Aug 6, 2026
Merged

feat(derived): encrypt flashcards, study guides, room summaries (#518)#528
AndresL230 merged 5 commits into
mainfrom
feat/522-d-518-derived

Conversation

@AndresL230

Copy link
Copy Markdown
Collaborator

Closes#518.

Derived content no longer leaks its encrypted sources:

  • flashcards.front/back — both insert paths (generated + imported); dedupe decrypts before Levenshtein
  • study_guides.content — encrypt_json; ETags unaffected (id+generated_at)
  • room_summaries.summary — cache keys on member_hash, unaffected
  • rollout-safe legacy-row reads (tested), backfill runners, encrypted seed, roundtrip tests, oracle entries

Stacked on #521's PR.

🤖 Generated with Claude Code

@coderabbitai

coderabbitaiBot commented Aug 6, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@AndresL230, you've reached your PR review limit, so we couldn't start this review.

Next review available in:53 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 388a06f1-7606-49ec-9f31-c6d9bf3b1f28

📥 Commits

Reviewing files that changed from the base of the PR and between c19a880 and 6bf1b18.

📒 Files selected for processing (13)
  • CLAUDE.md
  • backend/db/backfill_encryption.py
  • backend/db/seed_local_rich.py
  • backend/e2e_oracles/gather.py
  • backend/routes/flashcards.py
  • backend/routes/study_guide.py
  • backend/services/flashcard_import_service.py
  • backend/services/social_cache_service.py
  • backend/tests/integration/test_encryption_roundtrip.py
  • backend/tests/test_flashcard_import_service.py
  • backend/tests/test_flashcards_routes.py
  • backend/tests/test_social_cache_service.py
  • backend/tests/test_study_guide_encryption.py

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@supabase

supabaseBot commented Aug 6, 2026

Copy link
Copy Markdown

This pull request has been ignored for the connected project ybgqdonkoqftwrmweuyv because there are no changes detected in supabase directory. You can change this behaviour in Project Integrations Settings ↗︎.


Preview Branches by Supabase.
Learn more about Supabase Branching ↗︎.

@cloudflare-workers-and-pages

cloudflare-workers-and-pagesBot commented Aug 6, 2026

Copy link
Copy Markdown

Deploying with Cloudflare Workers Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

StatusNameLatest CommitPreview URLUpdated (UTC)
✅ Deployment successful!
View logs
frontend-staging6bf1b18Commit Preview URL

Branch Preview URL
Aug 06 2026, 01:36 AM

@AndresL230

Copy link
Copy Markdown
CollaboratorAuthor

Code review

No issues found. Checked for bugs and CLAUDE.md compliance: every encrypt/decrypt boundary traced (both flashcard insert paths return plaintext to the client; study-guide ETags stay on id+generated_at; room-summary cache keys on plaintext member_hash), dedupe decrypts before Levenshtein, legacy plaintext rows tolerated on every read path, and the seed/backfill/oracle/roundtrip artifacts carry matching column sets.

🤖 Generated with Claude Code

@AndresL230
AndresL230force-pushed the feat/522-d-518-derived branch 2 times, most recently from e167d28 to add403bCompareAugust 6, 2026 01:22
@AndresL230
AndresL230force-pushed the feat/522-c-521-quiz branch 2 times, most recently from b09c0e8 to 3e78b3eCompareAugust 6, 2026 01:25
@AndresL230
AndresL230force-pushed the feat/522-d-518-derived branch from add403b to 80a5a17CompareAugust 6, 2026 01:25
@AndresL230
AndresL230 changed the base branch from feat/522-c-521-quiz to mainAugust 6, 2026 01:28
AndresL230and others added 5 commits August 5, 2026 21:28
)
Both flashcards insert row-builders (AI-generated /generate, imported
/import/commit) now encrypt front/back before the row reaches Supabase.
The list read (GET /user/{user_id}) decrypts front/back for every
returned row, tolerating legacy plaintext rows via decrypt_if_present's
raw-value fallback. dedup_against_existing decrypts each existing row's
front before normalizing, so dedupe still matches against ciphertext
(and legacy plaintext) rows.
/generate's response is built from a decrypted copy of the inserted
rows (mirroring notes_service's write/read split) so the frontend still
sees plaintext front/back for cards it just generated, instead of the
ciphertext written to the row-builder.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Task 10 of the encryption-coverage epic: study_guides.content is now
ciphertext at write (encrypt_json) and decrypted at both readers
(get_cached_guides' list loop and the /guide cached-row return) via
decrypt_json_column, which also tolerates legacy plaintext dict rows.
The ETag on /cached still derives from id+generated_at only, computed
before decrypt.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…t encryption (#518)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…hed list (#518)
get_cached_guides ran decrypt_json_column unguarded per row — one
corrupt study_guides.content 500ed the WHOLE list instead of degrading
just that entry. Wrap the per-row decrypt in try/except, log a warning,
and fall back to content={} (exam_title/overview blank for that row
only) so the rest of the list still renders.
Extends tests/test_study_guide_encryption.py with a mixed-row case: one
good encrypted guide + one corrupt-content guide returns 200 with the
good guide's exam_title intact and the corrupt one's exam_title == ""
(verified to fail via stash/run/pop when the guard is reverted).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@AndresL230
AndresL230force-pushed the feat/522-d-518-derived branch from 80a5a17 to 6bf1b18CompareAugust 6, 2026 01:28
@AndresL230
AndresL230 merged commit 458ddb2 into mainAug 6, 2026
6 checks passed
Sign up for freeto 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.

Derived content is stored in plaintext while its encrypted source is protected (flashcards, study guides, room summaries)

1 participant

@AndresL230
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(derived): encrypt flashcards, study guides, room summaries (#518) - #528

Merged
AndresL230 merged 5 commits into
mainfrom
feat/522-d-518-derived
Aug 6, 2026
Merged

feat(derived): encrypt flashcards, study guides, room summaries (#518)#528
AndresL230 merged 5 commits into
mainfrom
feat/522-d-518-derived

Conversation

@AndresL230

Copy link
Copy Markdown
Collaborator

Closes#518.

Derived content no longer leaks its encrypted sources:

  • flashcards.front/back — both insert paths (generated + imported); dedupe decrypts before Levenshtein
  • study_guides.content — encrypt_json; ETags unaffected (id+generated_at)
  • room_summaries.summary — cache keys on member_hash, unaffected
  • rollout-safe legacy-row reads (tested), backfill runners, encrypted seed, roundtrip tests, oracle entries

Stacked on #521's PR.

🤖 Generated with Claude Code

@coderabbitai

coderabbitaiBot commented Aug 6, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@AndresL230, you've reached your PR review limit, so we couldn't start this review.

Next review available in:53 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 388a06f1-7606-49ec-9f31-c6d9bf3b1f28

📥 Commits

Reviewing files that changed from the base of the PR and between c19a880 and 6bf1b18.

📒 Files selected for processing (13)
  • CLAUDE.md
  • backend/db/backfill_encryption.py
  • backend/db/seed_local_rich.py
  • backend/e2e_oracles/gather.py
  • backend/routes/flashcards.py
  • backend/routes/study_guide.py
  • backend/services/flashcard_import_service.py
  • backend/services/social_cache_service.py
  • backend/tests/integration/test_encryption_roundtrip.py
  • backend/tests/test_flashcard_import_service.py
  • backend/tests/test_flashcards_routes.py
  • backend/tests/test_social_cache_service.py
  • backend/tests/test_study_guide_encryption.py

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@supabase

supabaseBot commented Aug 6, 2026

Copy link
Copy Markdown

This pull request has been ignored for the connected project ybgqdonkoqftwrmweuyv because there are no changes detected in supabase directory. You can change this behaviour in Project Integrations Settings ↗︎.


Preview Branches by Supabase.
Learn more about Supabase Branching ↗︎.

@cloudflare-workers-and-pages

cloudflare-workers-and-pagesBot commented Aug 6, 2026

Copy link
Copy Markdown

Deploying with Cloudflare Workers Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

StatusNameLatest CommitPreview URLUpdated (UTC)
✅ Deployment successful!
View logs
frontend-staging6bf1b18Commit Preview URL

Branch Preview URL
Aug 06 2026, 01:36 AM

@AndresL230

Copy link
Copy Markdown
CollaboratorAuthor

Code review

No issues found. Checked for bugs and CLAUDE.md compliance: every encrypt/decrypt boundary traced (both flashcard insert paths return plaintext to the client; study-guide ETags stay on id+generated_at; room-summary cache keys on plaintext member_hash), dedupe decrypts before Levenshtein, legacy plaintext rows tolerated on every read path, and the seed/backfill/oracle/roundtrip artifacts carry matching column sets.

🤖 Generated with Claude Code

@AndresL230
AndresL230force-pushed the feat/522-d-518-derived branch 2 times, most recently from e167d28 to add403bCompareAugust 6, 2026 01:22
@AndresL230
AndresL230force-pushed the feat/522-c-521-quiz branch 2 times, most recently from b09c0e8 to 3e78b3eCompareAugust 6, 2026 01:25
@AndresL230
AndresL230force-pushed the feat/522-d-518-derived branch from add403b to 80a5a17CompareAugust 6, 2026 01:25
@AndresL230
AndresL230 changed the base branch from feat/522-c-521-quiz to mainAugust 6, 2026 01:28
AndresL230and others added 5 commits August 5, 2026 21:28
)
Both flashcards insert row-builders (AI-generated /generate, imported
/import/commit) now encrypt front/back before the row reaches Supabase.
The list read (GET /user/{user_id}) decrypts front/back for every
returned row, tolerating legacy plaintext rows via decrypt_if_present's
raw-value fallback. dedup_against_existing decrypts each existing row's
front before normalizing, so dedupe still matches against ciphertext
(and legacy plaintext) rows.
/generate's response is built from a decrypted copy of the inserted
rows (mirroring notes_service's write/read split) so the frontend still
sees plaintext front/back for cards it just generated, instead of the
ciphertext written to the row-builder.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Task 10 of the encryption-coverage epic: study_guides.content is now
ciphertext at write (encrypt_json) and decrypted at both readers
(get_cached_guides' list loop and the /guide cached-row return) via
decrypt_json_column, which also tolerates legacy plaintext dict rows.
The ETag on /cached still derives from id+generated_at only, computed
before decrypt.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…t encryption (#518)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…hed list (#518)
get_cached_guides ran decrypt_json_column unguarded per row — one
corrupt study_guides.content 500ed the WHOLE list instead of degrading
just that entry. Wrap the per-row decrypt in try/except, log a warning,
and fall back to content={} (exam_title/overview blank for that row
only) so the rest of the list still renders.
Extends tests/test_study_guide_encryption.py with a mixed-row case: one
good encrypted guide + one corrupt-content guide returns 200 with the
good guide's exam_title intact and the corrupt one's exam_title == ""
(verified to fail via stash/run/pop when the guard is reverted).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@AndresL230
AndresL230force-pushed the feat/522-d-518-derived branch from 80a5a17 to 6bf1b18CompareAugust 6, 2026 01:28
@AndresL230
AndresL230 merged commit 458ddb2 into mainAug 6, 2026
6 checks passed
Sign up for freeto 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.

Derived content is stored in plaintext while its encrypted source is protected (flashcards, study guides, room summaries)

1 participant

@AndresL230
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

feat(derived): encrypt flashcards, study guides, room summaries (#518) - #528

Merged
AndresL230 merged 5 commits into
mainfrom
feat/522-d-518-derived
Aug 6, 2026
Merged

feat(derived): encrypt flashcards, study guides, room summaries (#518)#528
AndresL230 merged 5 commits into
mainfrom
feat/522-d-518-derived

Conversation

@AndresL230

Copy link
Copy Markdown
Collaborator

Closes#518.

Derived content no longer leaks its encrypted sources:

  • flashcards.front/back — both insert paths (generated + imported); dedupe decrypts before Levenshtein
  • study_guides.content — encrypt_json; ETags unaffected (id+generated_at)
  • room_summaries.summary — cache keys on member_hash, unaffected
  • rollout-safe legacy-row reads (tested), backfill runners, encrypted seed, roundtrip tests, oracle entries

Stacked on #521's PR.

🤖 Generated with Claude Code

@coderabbitai

coderabbitaiBot commented Aug 6, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@AndresL230, you've reached your PR review limit, so we couldn't start this review.

Next review available in:53 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 388a06f1-7606-49ec-9f31-c6d9bf3b1f28

📥 Commits

Reviewing files that changed from the base of the PR and between c19a880 and 6bf1b18.

📒 Files selected for processing (13)
  • CLAUDE.md
  • backend/db/backfill_encryption.py
  • backend/db/seed_local_rich.py
  • backend/e2e_oracles/gather.py
  • backend/routes/flashcards.py
  • backend/routes/study_guide.py
  • backend/services/flashcard_import_service.py
  • backend/services/social_cache_service.py
  • backend/tests/integration/test_encryption_roundtrip.py
  • backend/tests/test_flashcard_import_service.py
  • backend/tests/test_flashcards_routes.py
  • backend/tests/test_social_cache_service.py
  • backend/tests/test_study_guide_encryption.py

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@supabase

supabaseBot commented Aug 6, 2026

Copy link
Copy Markdown

This pull request has been ignored for the connected project ybgqdonkoqftwrmweuyv because there are no changes detected in supabase directory. You can change this behaviour in Project Integrations Settings ↗︎.


Preview Branches by Supabase.
Learn more about Supabase Branching ↗︎.

@cloudflare-workers-and-pages

cloudflare-workers-and-pagesBot commented Aug 6, 2026

Copy link
Copy Markdown

Deploying with Cloudflare Workers Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

StatusNameLatest CommitPreview URLUpdated (UTC)
✅ Deployment successful!
View logs
frontend-staging6bf1b18Commit Preview URL

Branch Preview URL
Aug 06 2026, 01:36 AM

@AndresL230

Copy link
Copy Markdown
CollaboratorAuthor

Code review

No issues found. Checked for bugs and CLAUDE.md compliance: every encrypt/decrypt boundary traced (both flashcard insert paths return plaintext to the client; study-guide ETags stay on id+generated_at; room-summary cache keys on plaintext member_hash), dedupe decrypts before Levenshtein, legacy plaintext rows tolerated on every read path, and the seed/backfill/oracle/roundtrip artifacts carry matching column sets.

🤖 Generated with Claude Code

@AndresL230
AndresL230force-pushed the feat/522-d-518-derived branch 2 times, most recently from e167d28 to add403bCompareAugust 6, 2026 01:22
@AndresL230
AndresL230force-pushed the feat/522-c-521-quiz branch 2 times, most recently from b09c0e8 to 3e78b3eCompareAugust 6, 2026 01:25
@AndresL230
AndresL230force-pushed the feat/522-d-518-derived branch from add403b to 80a5a17CompareAugust 6, 2026 01:25
@AndresL230
AndresL230 changed the base branch from feat/522-c-521-quiz to mainAugust 6, 2026 01:28
AndresL230and others added 5 commits August 5, 2026 21:28
)
Both flashcards insert row-builders (AI-generated /generate, imported
/import/commit) now encrypt front/back before the row reaches Supabase.
The list read (GET /user/{user_id}) decrypts front/back for every
returned row, tolerating legacy plaintext rows via decrypt_if_present's
raw-value fallback. dedup_against_existing decrypts each existing row's
front before normalizing, so dedupe still matches against ciphertext
(and legacy plaintext) rows.
/generate's response is built from a decrypted copy of the inserted
rows (mirroring notes_service's write/read split) so the frontend still
sees plaintext front/back for cards it just generated, instead of the
ciphertext written to the row-builder.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Task 10 of the encryption-coverage epic: study_guides.content is now
ciphertext at write (encrypt_json) and decrypted at both readers
(get_cached_guides' list loop and the /guide cached-row return) via
decrypt_json_column, which also tolerates legacy plaintext dict rows.
The ETag on /cached still derives from id+generated_at only, computed
before decrypt.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…t encryption (#518)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…hed list (#518)
get_cached_guides ran decrypt_json_column unguarded per row — one
corrupt study_guides.content 500ed the WHOLE list instead of degrading
just that entry. Wrap the per-row decrypt in try/except, log a warning,
and fall back to content={} (exam_title/overview blank for that row
only) so the rest of the list still renders.
Extends tests/test_study_guide_encryption.py with a mixed-row case: one
good encrypted guide + one corrupt-content guide returns 200 with the
good guide's exam_title intact and the corrupt one's exam_title == ""
(verified to fail via stash/run/pop when the guard is reverted).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@AndresL230
AndresL230force-pushed the feat/522-d-518-derived branch from 80a5a17 to 6bf1b18CompareAugust 6, 2026 01:28
@AndresL230
AndresL230 merged commit 458ddb2 into mainAug 6, 2026
6 checks passed
Sign up for freeto 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.

Derived content is stored in plaintext while its encrypted source is protected (flashcards, study guides, room summaries)

1 participant

@AndresL230