- Notifications
You must be signed in to change notification settings - Fork 0
fix: resolve 29 audit findings across clinical safety, privacy, worker, and api domains#2188
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Uh oh!
There was an error while loading. Please reload this page.
Changes from all commits
b393cddd2120b2511cda89f9c52da9f1d4fc87cc9457b576a41c49f5c94d9961f77721d67aa61e7221b51d1c9a8eb05b8779ff60712c764dc4a116f19993dbFile filter
Filter by extension
Conversations
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Uh oh!
There was an error while loading. Please reload this page.
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| | 2026-08-18 | claude/issues-reconcile-2026-08-19 | b393cdd530e81d6bfbdc15c495d870b57c73f01b | Serialized reconcile of 21 queued outstanding-issues inbox requests (PR #2168) | approved — documentation-only; canonical diff equals the recorded reconciliation transaction | check:outstanding-issues passed (392 rows, 57 open); check:ledger-write-discipline passed b400b138f8c1..HEAD; format:changed clean; inbox 0 pending / 391 applied | |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -58,6 +58,8 @@ export function subscribeFavouritesStorage(listener: () => void): () => void { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const FAVOURITES_TTL_MS = 90 * 24 * 60 * 60 * 1000; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| export function loadFavouriteLastOpened(): Record<string, number> { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| if (typeof window === "undefined") { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| return getDefaultInitialTimestamps(); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| @@ -69,7 +71,14 @@ export function loadFavouriteLastOpened(): Record<string, number> { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| if (raw) { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const parsed = JSON.parse(raw); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const result: Record<string, number> = { ...getDefaultInitialTimestamps(), ...parsed }; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const now = Date.now(); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const pruned: Record<string, number> = {}; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| for (const [key, ts] of Object.entries(parsed)) { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| if (typeof ts === "number" && Number.isFinite(ts) && now - ts < FAVOURITES_TTL_MS) { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| pruned[key] = ts; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const result: Record<string, number> = { ...getDefaultInitialTimestamps(), ...pruned }; | ||||||||||||||||||||||||||||||||||||||||||||||||||
Comment on lines
+74
to
+81
Contributor There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win Persist the pruned timestamps. The new code removes expired and invalid entries only from Proposed fix for (const [key, ts] of Object.entries(parsed)) {
if (typeof ts === "number" && Number.isFinite(ts) && now - ts < FAVOURITES_TTL_MS) {
pruned[key] = ts;
}
}
const result: Record<string, number> = { ...getDefaultInitialTimestamps(), ...pruned };
+ if (Object.keys(pruned).length !== Object.keys(parsed).length) {+ try {+ localStorage.setItem(DATABASE_FAVOURITES_LAST_OPENED_STORAGE_KEY, JSON.stringify(pruned));+ } catch {+ // Ignore storage write errors.+ }+ }
inMemoryLastOpened = result;📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents | ||||||||||||||||||||||||||||||||||||||||||||||||||
| inMemoryLastOpened = result; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| return result; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -15,7 +15,7 @@ const safetyPatterns: Array<{ kind: SafetyFindingKind; label: string; pattern: R | ||
| { | ||
| kind: "contraindication", | ||
| label: "Contraindication", | ||
| pattern: /\b(contraindicat|do not use|avoid|not recommended|must not)\b/i, | ||
| pattern: /\b(contraindicat\w*|do not use|avoid|not recommended|must not)\b/i, | ||
Contributor There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win Handle negated contraindication statements. The pattern matches 🤖 Prompt for AI Agents | ||
| }, | ||
| { | ||
| kind: "red_flag", | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -66,9 +66,11 @@ const termStatsCacheTtlMs = 10 * 60 * 1000; | ||
| const termStatsCacheMaxEntries = 1024; | ||
| const termStatsCache = new Map<string, { expiresAt: number; stats: CorpusTopicTermStats }>(); | ||
| const inFlightGroundingQueries = new Map<string, Promise<CorpusTopicTermStats[]>>(); | ||
| export function resetCorpusGroundingCacheForTests() { | ||
| termStatsCache.clear(); | ||
| inFlightGroundingQueries.clear(); | ||
| } | ||
| function cacheKey(ownerScopeKey: string, term: string) { | ||
| @@ -156,54 +158,63 @@ export async function classifyCorpusGrounding(args: { | ||
| } | ||
| if (missing.length > 0) { | ||
| const flightKey = `${ownerScopeKey}:${[...missing].sort().join(",")}`; | ||
| try { | ||
| const ownerFilter = accessScope.ownerId ?? PUBLIC_OWNER_FILTER_SENTINEL; | ||
| const versioned = await resolveAbortableQuery( | ||
| args.supabase.rpc("corpus_topic_term_stats_v2", { | ||
| terms: missing, | ||
| owner_filter: ownerFilter, | ||
| include_public: accessScope.includePublic, | ||
| }), | ||
| args.signal, | ||
| ); | ||
| const calls = | ||
| !versioned || isMissingRetrievalRpcError(versioned.error) | ||
| ? await Promise.all([ | ||
| resolveAbortableQuery( | ||
| args.supabase.rpc("corpus_topic_term_stats", { terms: missing, owner_filter: ownerFilter }), | ||
| args.signal, | ||
| ), | ||
| accessScope.ownerId && accessScope.includePublic | ||
| ? resolveAbortableQuery( | ||
| args.supabase.rpc("corpus_topic_term_stats", { | ||
| terms: missing, | ||
| owner_filter: PUBLIC_OWNER_FILTER_SENTINEL, | ||
| }), | ||
| args.signal, | ||
| ) | ||
| : Promise.resolve({ data: [], error: null }), | ||
| ]) | ||
| : [versioned]; | ||
| if (calls.some((call) => call.error)) throw calls.find((call) => call.error)?.error; | ||
| const byTerm = new Map<string, CorpusTopicTermStats>(); | ||
| for (const call of calls) { | ||
| for (const row of (call.data ?? []) as CorpusTopicTermStats[]) { | ||
| const current = byTerm.get(row.term); | ||
| byTerm.set( | ||
| row.term, | ||
| current | ||
| ? { | ||
| term: row.term, | ||
| has_ts_signal: current.has_ts_signal || row.has_ts_signal, | ||
| title_doc_count: current.title_doc_count + row.title_doc_count, | ||
| chunk_present: current.chunk_present || row.chunk_present, | ||
| total_doc_count: current.total_doc_count + row.total_doc_count, | ||
| } | ||
| : row, | ||
| let flight = inFlightGroundingQueries.get(flightKey); | ||
| if (!flight) { | ||
| flight = (async () => { | ||
| const ownerFilter = accessScope.ownerId ?? PUBLIC_OWNER_FILTER_SENTINEL; | ||
| const versioned = await resolveAbortableQuery( | ||
| args.supabase.rpc("corpus_topic_term_stats_v2", { | ||
| terms: missing, | ||
| owner_filter: ownerFilter, | ||
| include_public: accessScope.includePublic, | ||
| }), | ||
| args.signal, | ||
| ); | ||
Comment on lines
+161
to
174
Contributor There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win Keep shared-flight lifetime separate from caller cancellation. Line 173 binds the shared If the first caller aborts, the shared RPC rejects. Other active callers then return an Lines 231-232 also delete the shared entry when any caller returns. After per-caller abort support is added, this can start a duplicate RPC while the original shared RPC still runs. Create the shared RPC without a caller signal. Race each caller's wait against its own signal. Delete the map entry only when the shared RPC settles. Add focused regression coverage for aborting the creator and a later subscriber independently. Run the focused Also applies to: 217-232 🤖 Prompt for AI AgentsSource: Coding guidelines | ||
| } | ||
| const calls = | ||
| !versioned || isMissingRetrievalRpcError(versioned.error) | ||
| ? await Promise.all([ | ||
| resolveAbortableQuery( | ||
| args.supabase.rpc("corpus_topic_term_stats", { terms: missing, owner_filter: ownerFilter }), | ||
| args.signal, | ||
| ), | ||
| accessScope.ownerId && accessScope.includePublic | ||
| ? resolveAbortableQuery( | ||
| args.supabase.rpc("corpus_topic_term_stats", { | ||
| terms: missing, | ||
| owner_filter: PUBLIC_OWNER_FILTER_SENTINEL, | ||
| }), | ||
| args.signal, | ||
| ) | ||
| : Promise.resolve({ data: [], error: null }), | ||
| ]) | ||
| : [versioned]; | ||
| if (calls.some((call) => call.error)) throw calls.find((call) => call.error)?.error; | ||
| const byTerm = new Map<string, CorpusTopicTermStats>(); | ||
| for (const call of calls) { | ||
| for (const row of (call.data ?? []) as CorpusTopicTermStats[]) { | ||
| const current = byTerm.get(row.term); | ||
| byTerm.set( | ||
| row.term, | ||
| current | ||
| ? { | ||
| term: row.term, | ||
| has_ts_signal: current.has_ts_signal || row.has_ts_signal, | ||
| title_doc_count: current.title_doc_count + row.title_doc_count, | ||
| chunk_present: current.chunk_present || row.chunk_present, | ||
| total_doc_count: current.total_doc_count + row.total_doc_count, | ||
| } | ||
| : row, | ||
| ); | ||
| } | ||
| } | ||
| return [...byTerm.values()]; | ||
| })(); | ||
| inFlightGroundingQueries.set(flightKey, flight); | ||
| } | ||
| const rows = [...byTerm.values()]; | ||
| const rows = await flight; | ||
| // A term the RPC did not echo back got dropped SQL-side (blank after trim); treat the | ||
| // whole classification as inconclusive rather than guessing. | ||
| if (rows.length !== missing.length) return { verdict: "inconclusive", anchorTerms: [], absentTerms: [] }; | ||
| @@ -217,6 +228,8 @@ export async function classifyCorpusGrounding(args: { | ||
| // Fail open: missing RPC (migration not applied), transient DB error, demo mode — the | ||
| // caller keeps today's behaviour (LLM classifier fallback + soft-tail short-circuit). | ||
| return { verdict: "inconclusive", anchorTerms: [], absentTerms: [] }; | ||
| } finally { | ||
| inFlightGroundingQueries.delete(flightKey); | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
Repository: BigSimmo/Database
Length of output: 50373
🏁 Script executed:
Repository: BigSimmo/Database
Length of output: 4450
🏁 Script executed:
Repository: BigSimmo/Database
Length of output: 7874
🏁 Script executed:
Repository: BigSimmo/Database
Length of output: 8936
🏁 Script executed:
Repository: BigSimmo/Database
Length of output: 377
🏁 Script executed:
Repository: BigSimmo/Database
Length of output: 4663
🏁 Script executed:
Repository: BigSimmo/Database
Length of output: 359
Clean up the legacy auth-email value. If
AUTH_EMAIL_STORAGE_KEYexists inlocalStorage, remove it during the storage transition. Copy it tosessionStoragefirst only if saved-email compatibility is required. This code has no writer or remover to update.🤖 Prompt for AI Agents