Skip to content

feat(storage): encrypt content-bearing secondary databases - #310

Draft
qnbs wants to merge 4 commits into
mainfrom
feat/secure-secondary-idb
Draft

feat(storage): encrypt content-bearing secondary databases#310
qnbs wants to merge 4 commits into
mainfrom
feat/secure-secondary-idb

Conversation

@qnbs

@qnbs qnbs commented Aug 1, 2026

Copy link
Copy Markdown
Owner

Summary

  • add a shared versioned AES-256-GCM envelope for content-bearing secondary IndexedDB records
  • encrypt scene revisions, AI inference results, ProForge memory/history, cross-project metadata/embeddings, and LoRA metadata/datasets/training runs
  • fail closed while configured storage is locked, reject corrupt envelopes, and lazily migrate legacy plaintext after unlock
  • document the accepted DuckDB structural-metadata and large LoRA weight-blob exceptions
  • correct project documentation so cross-database passphrase rotation is tracked as a durable resumable follow-up, not described as atomic

Verification

  • pnpm run lint
  • pnpm run typecheck
  • pnpm run i18n:check (19 locales, 2,869 keys)
  • pnpm run parity:check
  • pnpm run docs:check
  • pnpm run suppressions:check (52/52)
  • pnpm run token:audit (160/160)
  • targeted Vitest: 109 existing/new secondary-store tests from the first commit
  • targeted Vitest: 53 Cross-Project/LoRA tests from the second commit

Security boundary

Large LoRA weight blobs remain outside the manuscript-data guarantee. DuckDB structural analytics metadata remains an accepted plaintext boundary; literal codex_mentions.excerpt encryption is unchanged. Durable resumable passphrase rotation across independent databases is intentionally deferred to the next isolated security change.

Copilot AI review requested due to automatic review settings August 1, 2026 18:17
@vercel

vercel Bot commented Aug 1, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
worldscript-studio Ready Ready Preview Aug 1, 2026 6:26pm

@coderabbitai

coderabbitai Bot commented Aug 1, 2026

Copy link
Copy Markdown

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your recent review volume is higher than typical usage, so adaptive limits are currently applied.

Next review available in: 29 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 93ca7c9a-1d38-4b19-9dce-aae1f8b682c5

📥 Commits

Reviewing files that changed from the base of the PR and between 804793a and 0898f6a.

📒 Files selected for processing (25)
  • AGENTS.md
  • CHANGELOG.md
  • CLAUDE.md
  • TODO.md
  • docs/IDB-ENCRYPTION.md
  • docs/SECURITY-THREAT-MODEL.md
  • services/ai/aiInferenceCacheService.ts
  • services/crossProjectIndexService.ts
  • services/loraAdapterService.ts
  • services/proForge/proForgeHistoryStore.ts
  • services/proForge/proForgeMemoryBank.ts
  • services/sceneRevisionService.ts
  • services/storage/idbAssetStore.ts
  • services/storage/idbCodexStore.ts
  • services/storage/idbPassphraseSentinel.ts
  • services/storage/storageEncryptionService.ts
  • tests/unit/aiInferenceCacheEncryption.test.ts
  • tests/unit/crossProjectIndexService.test.ts
  • tests/unit/lora/loraAdapterEncryption.test.ts
  • tests/unit/lora/loraAdapterService.test.ts
  • tests/unit/loraAdapterService.test.ts
  • tests/unit/proForge/proForgeHistoryStore.test.ts
  • tests/unit/proForge/proForgeMemoryBank.test.ts
  • tests/unit/sceneRevisionService.test.ts
  • tests/unit/storage/storageEncryptionService.test.ts

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

@codeant-ai

codeant-ai Bot commented Aug 1, 2026

Copy link
Copy Markdown

🏁 CodeAnt Quality Gate Results

Commit: 0898f6a4
Scan Time: 2026-08-01 18:27:55 UTC

✅ Overall Status: PASSED

Quality Gate Details

Quality Gate Status Details
Secrets ✅ PASSED 0 secrets found
Duplicate Code ✅ PASSED 0.0% duplicated
SAST ✅ PASSED No security issues
Bugs ✅ PASSED Rating S: No bugs
IAC ✅ PASSED No IAC issues

View Full Results

@deepsource-io

deepsource-io Bot commented Aug 1, 2026

Copy link
Copy Markdown

DeepSource Code Review

We reviewed changes in 804793a...0898f6a on this pull request. Below is the summary for the review, and you can see the individual issues we found as inline review comments.

See full review on DeepSource ↗

Important

Some issues found as part of this review are outside of the diff in this pull request and aren't shown in the inline review comments due to GitHub's API limitations. You can see those issues on the DeepSource dashboard.

PR Report Card

Overall Grade   Security  

Reliability  

Complexity  

Hygiene  

Code Review Summary

Analyzer Status Updated (UTC) Details
Docker Aug 1, 2026 6:25p.m. Review ↗
JavaScript Aug 1, 2026 6:25p.m. Review ↗
Python Aug 1, 2026 6:25p.m. Review ↗
Rust Aug 1, 2026 6:25p.m. Review ↗
Shell Aug 1, 2026 6:25p.m. Review ↗

Important

AI Review is run only on demand for your team. We're only showing results of static analysis review right now. To trigger AI Review, comment @deepsourcebot review on this thread.

Comment on lines +77 to +86
function projectSearchPayload(record: ProjectSearchIndex): ProjectSearchPayload {
return {
title: record.title,
logline: record.logline,
manuscriptWordCount: record.manuscriptWordCount,
characterNames: record.characterNames,
...(record.aiSummary !== undefined && { aiSummary: record.aiSummary }),
...(record.embeddingVector !== undefined && { embeddingVector: record.embeddingVector }),
};
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unexpected function declaration in the global scope, wrap in an IIFE for a local variable, assign as global property for a global variable


It is considered a best practice to avoid 'polluting' the global scope with variables that are intended to be local to the script. Global variables created from a script can produce name collisions with global variables created from another script, which will usually lead to runtime errors or unexpected behavior. It is mostly useful for browser scripts.

Comment on lines +88 to +97
async function encodeProjectSearchIndex(
record: ProjectSearchIndex,
): Promise<StoredProjectSearchIndex> {
return {
projectId: record.projectId,
lastIndexed: record.lastIndexed,
schemaVersion: RECORD_SCHEMA_VERSION,
payload: await prepareSecureRecordPayload(projectSearchPayload(record)),
};
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unexpected function declaration in the global scope, wrap in an IIFE for a local variable, assign as global property for a global variable


It is considered a best practice to avoid 'polluting' the global scope with variables that are intended to be local to the script. Global variables created from a script can produce name collisions with global variables created from another script, which will usually lead to runtime errors or unexpected behavior. It is mostly useful for browser scripts.

Comment on lines +99 to +123
async function decodeProjectSearchIndex(
stored: StoredProjectSearchIndex | ProjectSearchIndex,
): Promise<{ record: ProjectSearchIndex; needsMigration: boolean }> {
if ('payload' in stored) {
const decoded = await readSecureRecordPayload<ProjectSearchPayload>(stored.payload);
return {
record: {
projectId: stored.projectId,
lastIndexed: stored.lastIndexed,
...decoded.value,
},
needsMigration: decoded.needsMigration,
};
}

const decoded = await readSecureRecordPayload<ProjectSearchPayload>(projectSearchPayload(stored));
return {
record: {
projectId: stored.projectId,
lastIndexed: stored.lastIndexed,
...decoded.value,
},
needsMigration: decoded.needsMigration,
};
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unexpected function declaration in the global scope, wrap in an IIFE for a local variable, assign as global property for a global variable


It is considered a best practice to avoid 'polluting' the global scope with variables that are intended to be local to the script. Global variables created from a script can produce name collisions with global variables created from another script, which will usually lead to runtime errors or unexpected behavior. It is mostly useful for browser scripts.

Comment on lines +125 to +138
async function putProjectSearchIndexes(
db: IDBDatabase,
records: StoredProjectSearchIndex[],
): Promise<void> {
if (records.length === 0) return;
await new Promise<void>((resolve, reject) => {
const transaction = db.transaction(PROJECTS_INDEX_STORE, 'readwrite');
const store = transaction.objectStore(PROJECTS_INDEX_STORE);
for (const record of records) store.put(record);
transaction.oncomplete = () => resolve();
transaction.onerror = () => reject(transaction.error);
transaction.onabort = () => reject(transaction.error);
});
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unexpected function declaration in the global scope, wrap in an IIFE for a local variable, assign as global property for a global variable


It is considered a best practice to avoid 'polluting' the global scope with variables that are intended to be local to the script. Global variables created from a script can produce name collisions with global variables created from another script, which will usually lead to runtime errors or unexpected behavior. It is mostly useful for browser scripts.

Comment on lines 203 to 227
export async function listIndexedProjects(): Promise<ProjectSearchIndex[]> {
const db = await getDb();
const raw = await new Promise<Array<StoredProjectSearchIndex | ProjectSearchIndex>>(
(resolve, reject) => {
const tx = db.transaction(PROJECTS_INDEX_STORE, 'readonly');
const req = tx.objectStore(PROJECTS_INDEX_STORE).getAll();
req.onsuccess = () =>
resolve(req.result as Array<StoredProjectSearchIndex | ProjectSearchIndex>);
req.onerror = () => reject(req.error);
},
);

return new Promise((resolve, reject) => {
const tx = db.transaction(PROJECTS_INDEX_STORE, 'readonly');
const req = tx.objectStore(PROJECTS_INDEX_STORE).getAll();
req.onsuccess = () => {
const results = (req.result as ProjectSearchIndex[]).sort(
(a, b) => b.lastIndexed - a.lastIndexed,
);
resolve(results);
};
req.onerror = () => reject(req.error);
});
const records: ProjectSearchIndex[] = [];
const migrations: StoredProjectSearchIndex[] = [];
// QNBS-v3: Sequential decrypts keep large embedding collections within a stable memory bound.
for (const stored of raw) {
const decoded = await decodeProjectSearchIndex(stored);
records.push(decoded.record);
if (decoded.needsMigration) {
migrations.push(await encodeProjectSearchIndex(decoded.record));
}
}
await putProjectSearchIndexes(db, migrations);
return records.sort((a, b) => b.lastIndexed - a.lastIndexed);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unexpected function declaration in the global scope, wrap in an IIFE for a local variable, assign as global property for a global variable


It is considered a best practice to avoid 'polluting' the global scope with variables that are intended to be local to the script. Global variables created from a script can produce name collisions with global variables created from another script, which will usually lead to runtime errors or unexpected behavior. It is mostly useful for browser scripts.

Comment on lines +38 to +50
async function writeRawHistory(record: Record<string, unknown>): Promise<void> {
const db = await new Promise<IDBDatabase>((resolve, reject) => {
const request = indexedDB.open(DB_NAME, 1);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
await new Promise<void>((resolve, reject) => {
const transaction = db.transaction(STORE, 'readwrite');
transaction.objectStore(STORE).put(record);
transaction.oncomplete = () => resolve();
transaction.onerror = () => reject(transaction.error);
});
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unexpected function declaration in the global scope, wrap in an IIFE for a local variable, assign as global property for a global variable


It is considered a best practice to avoid 'polluting' the global scope with variables that are intended to be local to the script. Global variables created from a script can produce name collisions with global variables created from another script, which will usually lead to runtime errors or unexpected behavior. It is mostly useful for browser scripts.

Comment on lines +47 to +58
async function readRawMemoryEntry(id: string): Promise<Record<string, unknown> | undefined> {
const db = await new Promise<IDBDatabase>((resolve, reject) => {
const request = indexedDB.open(DB_NAME, 1);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
return new Promise((resolve, reject) => {
const request = db.transaction(STORE, 'readonly').objectStore(STORE).get(id);
request.onsuccess = () => resolve(request.result as Record<string, unknown> | undefined);
request.onerror = () => reject(request.error);
});
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unexpected function declaration in the global scope, wrap in an IIFE for a local variable, assign as global property for a global variable


It is considered a best practice to avoid 'polluting' the global scope with variables that are intended to be local to the script. Global variables created from a script can produce name collisions with global variables created from another script, which will usually lead to runtime errors or unexpected behavior. It is mostly useful for browser scripts.

Comment on lines +60 to +72
async function writeRawMemoryEntry(record: Record<string, unknown>): Promise<void> {
const db = await new Promise<IDBDatabase>((resolve, reject) => {
const request = indexedDB.open(DB_NAME, 1);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
await new Promise<void>((resolve, reject) => {
const transaction = db.transaction(STORE, 'readwrite');
transaction.objectStore(STORE).put(record);
transaction.oncomplete = () => resolve();
transaction.onerror = () => reject(transaction.error);
});
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unexpected function declaration in the global scope, wrap in an IIFE for a local variable, assign as global property for a global variable


It is considered a best practice to avoid 'polluting' the global scope with variables that are intended to be local to the script. Global variables created from a script can produce name collisions with global variables created from another script, which will usually lead to runtime errors or unexpected behavior. It is mostly useful for browser scripts.

Comment on lines +25 to +36
async function readRawRevision(id: string): Promise<unknown> {
const db = await new Promise<IDBDatabase>((resolve, reject) => {
const request = indexedDB.open(DB_NAME, 1);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
return new Promise((resolve, reject) => {
const request = db.transaction(STORE, 'readonly').objectStore(STORE).get(id);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unexpected function declaration in the global scope, wrap in an IIFE for a local variable, assign as global property for a global variable


It is considered a best practice to avoid 'polluting' the global scope with variables that are intended to be local to the script. Global variables created from a script can produce name collisions with global variables created from another script, which will usually lead to runtime errors or unexpected behavior. It is mostly useful for browser scripts.

Comment on lines +38 to +50
async function writeRawRevision(record: unknown): Promise<void> {
const db = await new Promise<IDBDatabase>((resolve, reject) => {
const request = indexedDB.open(DB_NAME, 1);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
await new Promise<void>((resolve, reject) => {
const transaction = db.transaction(STORE, 'readwrite');
transaction.objectStore(STORE).put(record);
transaction.oncomplete = () => resolve();
transaction.onerror = () => reject(transaction.error);
});
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unexpected function declaration in the global scope, wrap in an IIFE for a local variable, assign as global property for a global variable


It is considered a best practice to avoid 'polluting' the global scope with variables that are intended to be local to the script. Global variables created from a script can produce name collisions with global variables created from another script, which will usually lead to runtime errors or unexpected behavior. It is mostly useful for browser scripts.

Comment on lines +77 to +86
function projectSearchPayload(record: ProjectSearchIndex): ProjectSearchPayload {
return {
title: record.title,
logline: record.logline,
manuscriptWordCount: record.manuscriptWordCount,
characterNames: record.characterNames,
...(record.aiSummary !== undefined && { aiSummary: record.aiSummary }),
...(record.embeddingVector !== undefined && { embeddingVector: record.embeddingVector }),
};
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unexpected function declaration in the global scope, wrap in an IIFE for a local variable, assign as global property for a global variable


It is considered a best practice to avoid 'polluting' the global scope with variables that are intended to be local to the script. Global variables created from a script can produce name collisions with global variables created from another script, which will usually lead to runtime errors or unexpected behavior. It is mostly useful for browser scripts.

Comment on lines +88 to +97
async function encodeProjectSearchIndex(
record: ProjectSearchIndex,
): Promise<StoredProjectSearchIndex> {
return {
projectId: record.projectId,
lastIndexed: record.lastIndexed,
schemaVersion: RECORD_SCHEMA_VERSION,
payload: await prepareSecureRecordPayload(projectSearchPayload(record)),
};
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unexpected function declaration in the global scope, wrap in an IIFE for a local variable, assign as global property for a global variable


It is considered a best practice to avoid 'polluting' the global scope with variables that are intended to be local to the script. Global variables created from a script can produce name collisions with global variables created from another script, which will usually lead to runtime errors or unexpected behavior. It is mostly useful for browser scripts.

Comment on lines +99 to +123
async function decodeProjectSearchIndex(
stored: StoredProjectSearchIndex | ProjectSearchIndex,
): Promise<{ record: ProjectSearchIndex; needsMigration: boolean }> {
if ('payload' in stored) {
const decoded = await readSecureRecordPayload<ProjectSearchPayload>(stored.payload);
return {
record: {
projectId: stored.projectId,
lastIndexed: stored.lastIndexed,
...decoded.value,
},
needsMigration: decoded.needsMigration,
};
}

const decoded = await readSecureRecordPayload<ProjectSearchPayload>(projectSearchPayload(stored));
return {
record: {
projectId: stored.projectId,
lastIndexed: stored.lastIndexed,
...decoded.value,
},
needsMigration: decoded.needsMigration,
};
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unexpected function declaration in the global scope, wrap in an IIFE for a local variable, assign as global property for a global variable


It is considered a best practice to avoid 'polluting' the global scope with variables that are intended to be local to the script. Global variables created from a script can produce name collisions with global variables created from another script, which will usually lead to runtime errors or unexpected behavior. It is mostly useful for browser scripts.

Comment on lines +125 to +138
async function putProjectSearchIndexes(
db: IDBDatabase,
records: StoredProjectSearchIndex[],
): Promise<void> {
if (records.length === 0) return;
await new Promise<void>((resolve, reject) => {
const transaction = db.transaction(PROJECTS_INDEX_STORE, 'readwrite');
const store = transaction.objectStore(PROJECTS_INDEX_STORE);
for (const record of records) store.put(record);
transaction.oncomplete = () => resolve();
transaction.onerror = () => reject(transaction.error);
transaction.onabort = () => reject(transaction.error);
});
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unexpected function declaration in the global scope, wrap in an IIFE for a local variable, assign as global property for a global variable


It is considered a best practice to avoid 'polluting' the global scope with variables that are intended to be local to the script. Global variables created from a script can produce name collisions with global variables created from another script, which will usually lead to runtime errors or unexpected behavior. It is mostly useful for browser scripts.

Comment on lines 203 to 227
export async function listIndexedProjects(): Promise<ProjectSearchIndex[]> {
const db = await getDb();
const raw = await new Promise<Array<StoredProjectSearchIndex | ProjectSearchIndex>>(
(resolve, reject) => {
const tx = db.transaction(PROJECTS_INDEX_STORE, 'readonly');
const req = tx.objectStore(PROJECTS_INDEX_STORE).getAll();
req.onsuccess = () =>
resolve(req.result as Array<StoredProjectSearchIndex | ProjectSearchIndex>);
req.onerror = () => reject(req.error);
},
);

return new Promise((resolve, reject) => {
const tx = db.transaction(PROJECTS_INDEX_STORE, 'readonly');
const req = tx.objectStore(PROJECTS_INDEX_STORE).getAll();
req.onsuccess = () => {
const results = (req.result as ProjectSearchIndex[]).sort(
(a, b) => b.lastIndexed - a.lastIndexed,
);
resolve(results);
};
req.onerror = () => reject(req.error);
});
const records: ProjectSearchIndex[] = [];
const migrations: StoredProjectSearchIndex[] = [];
// QNBS-v3: Sequential decrypts keep large embedding collections within a stable memory bound.
for (const stored of raw) {
const decoded = await decodeProjectSearchIndex(stored);
records.push(decoded.record);
if (decoded.needsMigration) {
migrations.push(await encodeProjectSearchIndex(decoded.record));
}
}
await putProjectSearchIndexes(db, migrations);
return records.sort((a, b) => b.lastIndexed - a.lastIndexed);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unexpected function declaration in the global scope, wrap in an IIFE for a local variable, assign as global property for a global variable


It is considered a best practice to avoid 'polluting' the global scope with variables that are intended to be local to the script. Global variables created from a script can produce name collisions with global variables created from another script, which will usually lead to runtime errors or unexpected behavior. It is mostly useful for browser scripts.

Comment on lines +38 to +50
async function writeRawHistory(record: Record<string, unknown>): Promise<void> {
const db = await new Promise<IDBDatabase>((resolve, reject) => {
const request = indexedDB.open(DB_NAME, 1);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
await new Promise<void>((resolve, reject) => {
const transaction = db.transaction(STORE, 'readwrite');
transaction.objectStore(STORE).put(record);
transaction.oncomplete = () => resolve();
transaction.onerror = () => reject(transaction.error);
});
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unexpected function declaration in the global scope, wrap in an IIFE for a local variable, assign as global property for a global variable


It is considered a best practice to avoid 'polluting' the global scope with variables that are intended to be local to the script. Global variables created from a script can produce name collisions with global variables created from another script, which will usually lead to runtime errors or unexpected behavior. It is mostly useful for browser scripts.

Comment on lines +47 to +58
async function readRawMemoryEntry(id: string): Promise<Record<string, unknown> | undefined> {
const db = await new Promise<IDBDatabase>((resolve, reject) => {
const request = indexedDB.open(DB_NAME, 1);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
return new Promise((resolve, reject) => {
const request = db.transaction(STORE, 'readonly').objectStore(STORE).get(id);
request.onsuccess = () => resolve(request.result as Record<string, unknown> | undefined);
request.onerror = () => reject(request.error);
});
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unexpected function declaration in the global scope, wrap in an IIFE for a local variable, assign as global property for a global variable


It is considered a best practice to avoid 'polluting' the global scope with variables that are intended to be local to the script. Global variables created from a script can produce name collisions with global variables created from another script, which will usually lead to runtime errors or unexpected behavior. It is mostly useful for browser scripts.

Comment on lines +60 to +72
async function writeRawMemoryEntry(record: Record<string, unknown>): Promise<void> {
const db = await new Promise<IDBDatabase>((resolve, reject) => {
const request = indexedDB.open(DB_NAME, 1);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
await new Promise<void>((resolve, reject) => {
const transaction = db.transaction(STORE, 'readwrite');
transaction.objectStore(STORE).put(record);
transaction.oncomplete = () => resolve();
transaction.onerror = () => reject(transaction.error);
});
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unexpected function declaration in the global scope, wrap in an IIFE for a local variable, assign as global property for a global variable


It is considered a best practice to avoid 'polluting' the global scope with variables that are intended to be local to the script. Global variables created from a script can produce name collisions with global variables created from another script, which will usually lead to runtime errors or unexpected behavior. It is mostly useful for browser scripts.

Comment on lines +25 to +36
async function readRawRevision(id: string): Promise<unknown> {
const db = await new Promise<IDBDatabase>((resolve, reject) => {
const request = indexedDB.open(DB_NAME, 1);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
return new Promise((resolve, reject) => {
const request = db.transaction(STORE, 'readonly').objectStore(STORE).get(id);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unexpected function declaration in the global scope, wrap in an IIFE for a local variable, assign as global property for a global variable


It is considered a best practice to avoid 'polluting' the global scope with variables that are intended to be local to the script. Global variables created from a script can produce name collisions with global variables created from another script, which will usually lead to runtime errors or unexpected behavior. It is mostly useful for browser scripts.

Comment on lines +38 to +50
async function writeRawRevision(record: unknown): Promise<void> {
const db = await new Promise<IDBDatabase>((resolve, reject) => {
const request = indexedDB.open(DB_NAME, 1);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
await new Promise<void>((resolve, reject) => {
const transaction = db.transaction(STORE, 'readwrite');
transaction.objectStore(STORE).put(record);
transaction.oncomplete = () => resolve();
transaction.onerror = () => reject(transaction.error);
});
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unexpected function declaration in the global scope, wrap in an IIFE for a local variable, assign as global property for a global variable


It is considered a best practice to avoid 'polluting' the global scope with variables that are intended to be local to the script. Global variables created from a script can produce name collisions with global variables created from another script, which will usually lead to runtime errors or unexpected behavior. It is mostly useful for browser scripts.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Extends AES-256-GCM encryption to content-bearing secondary IndexedDB stores and documents remaining storage-security boundaries.

Changes:

  • Adds versioned encrypted record envelopes with locked/corrupt failure handling.
  • Integrates encryption and lazy migration across secondary stores.
  • Adds focused security tests and updates encryption documentation.

Reviewed changes

Copilot reviewed 25 out of 25 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
TODO.md Updates release and security follow-up status.
tests/unit/storage/storageEncryptionService.test.ts Tests secure envelope behavior.
tests/unit/sceneRevisionService.test.ts Tests encrypted scene revisions.
tests/unit/proForge/proForgeMemoryBank.test.ts Tests encrypted memory entries.
tests/unit/proForge/proForgeHistoryStore.test.ts Tests encrypted run histories.
tests/unit/loraAdapterService.test.ts Resets cached LoRA database handles.
tests/unit/lora/loraAdapterService.test.ts Improves LoRA test isolation.
tests/unit/lora/loraAdapterEncryption.test.ts Tests encrypted LoRA records.
tests/unit/crossProjectIndexService.test.ts Tests encrypted project indexes.
tests/unit/aiInferenceCacheEncryption.test.ts Tests encrypted inference results.
services/storage/storageEncryptionService.ts Implements secondary-record envelopes.
services/storage/idbPassphraseSentinel.ts Adds test connection reset support.
services/storage/idbCodexStore.ts Corrects encryption documentation.
services/storage/idbAssetStore.ts Corrects asset-encryption documentation.
services/sceneRevisionService.ts Encrypts revision payloads.
services/proForge/proForgeMemoryBank.ts Encrypts memory-bank payloads.
services/proForge/proForgeHistoryStore.ts Encrypts pipeline histories.
services/loraAdapterService.ts Encrypts LoRA metadata and training data.
services/crossProjectIndexService.ts Encrypts descriptive index data.
services/ai/aiInferenceCacheService.ts Encrypts persisted inference results.
docs/SECURITY-THREAT-MODEL.md Updates encryption boundaries and risks.
docs/IDB-ENCRYPTION.md Documents formats, coverage, and rotation limits.
CLAUDE.md Updates IDB encryption status.
CHANGELOG.md Records the security enhancement.
AGENTS.md Updates contributor security guidance.

Comment on lines +251 to +254
export async function prepareSecureRecordPayload<T>(value: T): Promise<T | SecureRecordEnvelope> {
if (_activeKey) {
const blob = await _svc.encrypt(_activeKey, value);
return encryptedBlobToEnvelope(blob.bytes);
Comment on lines +205 to +209
function isSecureRecordCandidate(value: unknown): boolean {
if (typeof value !== 'object' || value === null) return false;
const record = value as Record<string, unknown>;
return 'version' in record && 'iv' in record && 'ciphertext' in record;
}
Comment on lines +205 to +214
function isSecureRecordCandidate(value: unknown): boolean {
if (typeof value !== 'object' || value === null) return false;
const record = value as Record<string, unknown>;
if ('iv' in record || 'ciphertext' in record) return true;
if (!('version' in record)) return false;
// QNBS-v3: Reserve envelope-only shapes so missing fields fail closed without misclassifying payloads that merely have a domain `version` field.
return Object.keys(record).every(
(key) => key === 'version' || key === 'iv' || key === 'ciphertext',
);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unexpected function declaration in the global scope, wrap in an IIFE for a local variable, assign as global property for a global variable


It is considered a best practice to avoid 'polluting' the global scope with variables that are intended to be local to the script. Global variables created from a script can produce name collisions with global variables created from another script, which will usually lead to runtime errors or unexpected behavior. It is mostly useful for browser scripts.

return true;
}

function isSecureRecordCandidate(value: unknown): boolean {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

`isSecureRecordCandidate` has a cyclomatic complexity of 6 with "medium" risk


A function with high cyclomatic complexity can be hard to understand and
maintain. Cyclomatic complexity is a software metric that measures the number of
independent paths through a function. A higher cyclomatic complexity indicates
that the function has more decision points and is more complex.

@qnbs
qnbs marked this pull request as draft August 1, 2026 18:27

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6b96baa4c2

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +250 to +256
/** Prepare a secondary-store payload without ever downgrading configured encryption to plaintext. */
export async function prepareSecureRecordPayload<T>(value: T): Promise<T | SecureRecordEnvelope> {
if (_activeKey) {
const blob = await _svc.encrypt(_activeKey, value);
return encryptedBlobToEnvelope(blob.bytes);
}
if (await isSecureStorageConfigured()) throw new SecureRecordLockedError();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Gate destructive writes while the encryption key is locked

When a user locks the session and then deletes a LoRA adapter, scene revision, project index, or memory entry, deleteAdapter, deleteRevision, removeProjectIndex, and deleteMemoryEntry issue raw IndexedDB deletes without passing through this configured-storage check, so destructive writes still succeed while the secondary stores are supposedly locked. Apply the same sentinel/key guard before these delete transactions to prevent irreversible data loss while locked.

AGENTS.md reference: AGENTS.md:L370-L370

Useful? React with 👍 / 👎.

Comment on lines +205 to +208
function isSecureRecordCandidate(value: unknown): boolean {
if (typeof value !== 'object' || value === null) return false;
const record = value as Record<string, unknown>;
return 'version' in record && 'iv' in record && 'ciphertext' in record;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Treat partial secure envelopes as corruption

If an encrypted payload loses any one envelope field—for example, a missing ciphertext property—this predicate returns false. While the key is unlocked, readSecureRecordPayload consequently accepts the malformed object as legacy plaintext, and several callers spread and lazily rewrite it, potentially returning records without their required content and persisting that damage instead of raising SecureRecordCorruptError. Treat the presence of the version marker or any envelope field as a candidate, then validate the complete shape.

Useful? React with 👍 / 👎.

Comment on lines +264 to +266
if (isSecureRecordCandidate(stored)) {
if (!isSecureRecordEnvelope(stored)) throw new SecureRecordCorruptError();
if (!_activeKey) throw new SecureRecordLockedError();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Decrypt secondary records before disabling encryption

After a user enables encryption and writes any of these new secondary envelopes, the existing Settings disable flow verifies the passphrase and then calls clearIdbPassphrase(), which removes both the sentinel and active key without decrypting these databases. Subsequent reads still enter this envelope branch and always throw SecureRecordLockedError, while the disabled feature flag prevents the startup unlock modal from appearing, so revisions, ProForge history, search metadata, and LoRA records become inaccessible through the normal UI. The disable operation must rewrite secondary envelopes to plaintext before clearing the key and sentinel.

Useful? React with 👍 / 👎.

Comment on lines +251 to +254
export async function prepareSecureRecordPayload<T>(value: T): Promise<T | SecureRecordEnvelope> {
if (_activeKey) {
const blob = await _svc.encrypt(_activeKey, value);
return encryptedBlobToEnvelope(blob.bytes);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Bind encrypted payloads to their routing fields

Because encryption receives only the payload, the AES-GCM authentication tag is not bound to the plaintext record ID, project ID, scene ID, or timestamps stored alongside it. Swapping two valid payload envelopes in IndexedDB therefore decrypts successfully and silently associates one project's or scene's content with another record instead of reporting corruption. Pass canonical routing fields as AES-GCM additionalData during encryption and require the same data during decryption.

Useful? React with 👍 / 👎.

Comment on lines +66 to +70
async function encodeHistory(projectId: string, runs: PipelineRun[]): Promise<HistoryRecord> {
return {
projectId,
schemaVersion: RECORD_SCHEMA_VERSION,
payload: await prepareSecureRecordPayload({ runs: runs.slice(0, MAX_RUN_HISTORY) }),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve Blob artifacts when encrypting run history

When an encrypted ProForge run includes the production stage, its agentOutput contains a ProductionManifest whose artifacts carry generated PDF, EPUB, and Markdown Blob values. prepareSecureRecordPayload serializes this entire history through JSON.stringify, which converts each Blob to {}; after reload the decrypted history therefore contains invalid artifact objects and the generated files are silently lost, whereas the previous IndexedDB structured-clone write preserved them. Encode Blob bytes and MIME types explicitly or use a serialization format that supports structured-clone values.

Useful? React with 👍 / 👎.

@qnbs

qnbs commented Aug 1, 2026

Copy link
Copy Markdown
Owner Author

Handoff — paused to conserve the remaining weekly agent budget

Current head: 0898f6a4 (25 changed files). This PR is intentionally Draft and must not merge yet.

Completed

  • Shared versioned AES-256-GCM secondary-record envelope with typed locked/corrupt failures.
  • Scene revisions, AI inference cache, ProForge memory/history, cross-project metadata/embeddings, and LoRA metadata/datasets/training records integrated.
  • Legacy plaintext lazy migration and raw-IDB canary coverage added.
  • Security boundary/docs reconciled; DuckDB structural metadata and large LoRA weight blobs remain explicit exceptions.
  • Follow-up 0898f6a4 fixes malformed/incomplete envelope fall-through; 46/46 focused storage-encryption tests and lint are green.
  • Earlier local gates were green: lint, typecheck, i18n (19 × 2,869), parity, docs, suppressions 52/52, tokens 160/160; targeted store tests were green.

Release blocker

Copilot correctly identified that the live passphrase-change and encryption-disable flows do not migrate/decrypt the new secondary envelopes. Changing the passphrase replaces the active verifier/key after only primary app-data/snapshot migration; disabling deletes the verifier. Either operation can strand secondary records under the old key. See #310 (comment).

Before marking ready, choose and test one safe path:

  1. Preferred: implement the durable resumable cross-database rotation journal already specified in docs/IDB-ENCRYPTION.md, and make disable decrypt/migrate every registered secondary store before removing the verifier; or
  2. Minimal interim safety: block passphrase change and disable in both UI and service-level APIs while secondary encryption is present, with honest user-facing copy, until the journal ships.

Also test interruption/restart, wrong old/new passphrase, mixed old/new records, failure before verifier replacement, and disable recovery. Do not merely hide UI controls while callable service paths remain destructive.

Reviews / CI

  • CodeRabbit auto-triggered but was rate-limited (no code review); retry after the cooldown shown by its PR note.
  • Copilot produced two findings: the rotation/disable blocker above remains open; incomplete-envelope fall-through is fixed by 0898f6a4 and its thread is now outdated.
  • DeepSource generated ~126 mostly identical Unexpected function declaration in the global scope findings against normal TypeScript ES modules, plus minor complexity notices. Treat the module-scope reports as analyzer/configuration false positives; do not wrap the codebase in IIFEs. They still need evidence replies/resolution when work resumes.
  • Cloud CI restarted for 0898f6a4 and was still pending at pause time. Recheck every required job and all paginated review threads before ready/merge.

Resume order

  1. Resolve the passphrase rotation/disable blocker with tests.
  2. Run only targeted local Vitest plus sequential quick gates.
  3. Push; inspect CI artifacts and all 128+ paginated review threads.
  4. Reply/resolve each valid or false-positive thread with evidence; retry CodeRabbit after its cooldown and repeat until a fresh review yields zero new comments.
  5. Mark ready only when CI is green and unresolved thread count is zero. Merge via the already authorized admin bypass only then.
  6. Do not cut a release from this draft. A future v1.27.0 boundary is appropriate only after rotation and durable-log hardening are complete.

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