fix: GDPR and ISO 27001 security remediation#12
Conversation
Security & privacy fixes across the full stack: **Data Privacy (GDPR)** - Remove email from public getPage response (data minimisation) - Add consent field (z.literal(true)) to contact form submission - Add ConsentBanner component with localStorage acceptance tracking - Gate analytics trackView on user consent via hasAnalyticsConsent() - Truncate userAgent to browser/version only before DB insert - Add privacy policy URL threading to ConnectBlock consent checkbox - Improve stripHtml with entity-decode pass to prevent bypass - Add truncateUserAgent() utility for GDPR-compliant UA storage **Security Headers** - Add CSP header to Next.js (replaces TODO comment) - Add X-Frame-Options: DENY to Hono server - Add Referrer-Policy: strict-origin-when-cross-origin - Add Permissions-Policy (camera, mic, geolocation, payment) **Auth & Access Control** - Add Next.js edge middleware for /admin/* route protection - Redirect unauthenticated users to /admin/login with ?from= param **Secret Masking** - Mask wallet secrets (signer_cert, signer_key, wwdr_cert) in wallet.getConfig - Mask secrets in settings.get endpoint (matching getAll behaviour) - Exclude wallet secrets from backup export **Audit Logging** - Add audit_log DB schema with action/resource/details/created_at - Add logAudit() utility with silent failure so it never breaks ops - Wire audit logging into settings, backup, and contacts mutations **Data Retention** - Add purgeExpiredData procedure to analytics router (90-day cutoff) **Infrastructure** - Add npm audit step to CI (continue-on-error: true) - Add Docker healthcheck to docker-compose.yml - Change default Alchemy password to CHANGE-ME-BEFORE-DEPLOY **Input Validation** - Validate and escape email in reset-password script (prevent SQL injection) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
Caution Review failedThe pull request is closed. ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (13)
WalkthroughAdds consent banner & client consent gating, admin edge middleware, audit logging infrastructure and DB migration, security headers and CSP, settings/wallet secret masking, sanitization and UA truncation, contact form consent, data retention purge mutation, CI audit step, and a docker healthcheck. Changes
Sequence Diagram(s)sequenceDiagram
actor User
participant Page as Home Page
participant Banner as Consent Banner
participant Storage as localStorage
participant Analytics as Analytics API
User->>Page: Visit homepage
Page->>Banner: Render ConsentBanner
Banner->>Storage: read CONSENT_KEY
alt no stored value
Banner->>User: show Accept/Decline
User->>Banner: Accept
Banner->>Storage: write "accepted"
end
Page->>Analytics: trackView() only if hasAnalyticsConsent()
Analytics-->>Page: ack
sequenceDiagram
actor Client
participant Middleware as Next Edge Middleware
participant Cookies as Request Cookies
participant Login as /admin/login
Client->>Middleware: Request /admin/some
Middleware->>Middleware: allow if /admin/login or /admin/setup
alt other /admin/*
Middleware->>Cookies: check better-auth.session_token
alt missing
Middleware->>Login: redirect to /admin/login?from=...
else present
Middleware-->>Client: NextResponse.next()
end
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
|
@coderabbitai full review |
✅ Actions performedFull review triggered. |
There was a problem hiding this comment.
Actionable comments posted: 8
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
packages/api/src/routers/public.ts (1)
121-131: 🛠️ Refactor suggestion | 🟠 MajorMove
submitContactvalidation to shared validators.This mutation extends an inline schema instead of using a shared schema from
packages/validators, which drifts server/client validation over time.As per coding guidelines:
**/{apps/web,apps/server,packages/api}/**/*.{ts,tsx,js,jsx}: Use shared Zod validation schemas frompackages/validatorsfor form and API validation.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/api/src/routers/public.ts` around lines 121 - 131, Replace the inline Zod schema used in the submitContact mutation with the shared schema from packages/validators: import the shared contact/form schema (e.g., ContactFormSchema or SubmitContactSchema) and use it in the mutation input instead of the current z.object({...}) block; ensure the shared schema includes the same fields (firstName, lastName, email, whereMet, message?, captchaToken?, blockId?, blockTitle?, consent literal true) and update the import at the top of the file while removing the inline schema to keep server/client validation consistent.scripts/reset-password.ts (1)
34-42:⚠️ Potential issue | 🔴 CriticalFix shell injection vulnerability in password reset command execution.
The email regex validation (line 22) does not exclude shell metacharacters like
$, backticks, and others. Even with SQL escaping of single quotes, an attacker can inject shell commands through the email parameter. For example, an email liketest$(id)@test.compasses validation but triggers command substitution when the SQL is embedded in the double-quoted shell string at line 37, leading to arbitrary code execution.Use
execFileSyncwith an argument array instead of shell string interpolation to prevent this vulnerability:Safer execution pattern
-import { execSync } from "node:child_process"; +import { execFileSync } from "node:child_process"; @@ -const remoteFlag = remote ? " --remote" : ""; -const cmd = `npx wrangler d1 execute linkden-db --command "${sql}"${remoteFlag}`; @@ try { - execSync(cmd, { stdio: "inherit", cwd: "apps/server" }); + const wranglerArgs = ["wrangler", "d1", "execute", "linkden-db", "--command", sql]; + if (remote) { + wranglerArgs.push("--remote"); + } + execFileSync("npx", wranglerArgs, { stdio: "inherit", cwd: "apps/server" }); console.log("Password reset successfully."); } catch {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@scripts/reset-password.ts` around lines 34 - 42, The command construction using a shell-interpolated string (variables sql, cmd) and execSync allows shell injection via email (even if escaped for SQL); replace the execSync call with a safe execFileSync/child_process.spawnSync usage that passes the wrangler binary and its args as an array (avoid any single combined shell string), build the SQL string separately (using escapedHash/escapedEmail) but pass it as a single argument like ["d1","execute","linkden-db","--command", sql, ...(remote ? ["--remote"] : [])] to prevent shell interpretation, and update the import/usage from execSync to execFileSync/ spawnSync accordingly so no shell interpolation occurs.
🧹 Nitpick comments (5)
.github/workflows/ci.yml (1)
23-25: Make vulnerability auditing enforceable on protected branches.Line 25 keeps the audit step non-blocking, so moderate+ findings won’t stop merges. That weakens the remediation objective.
Suggested CI tightening
- name: Audit dependencies run: npm audit --audit-level=moderate - continue-on-error: true🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In @.github/workflows/ci.yml around lines 23 - 25, The "Audit dependencies" CI step currently runs "npm audit --audit-level=moderate" with "continue-on-error: true", making vulnerability findings non-blocking; change this so audits fail the job on protected branches by removing or setting "continue-on-error" to false for that step (or add a branch conditional that enforces failure when github.ref matches protected branches) and ensure the step continues to use "npm audit --audit-level=moderate" (or raise the audit level if desired) so moderate+ findings will block merges; update the step named "Audit dependencies" to enforce failure accordingly.apps/web/next.config.ts (1)
23-26:frame-src *is very permissive—verify this is intentional.Allowing frames from any origin may be necessary for embedding third-party content (e.g., YouTube, Spotify blocks), but it weakens clickjacking protection provided by
X-Frame-Options: DENY. Consider restricting to known embed domains if possible.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/web/next.config.ts` around lines 23 - 26, The CSP entry with key "Content-Security-Policy" currently uses the very permissive directive frame-src *; update this policy in next.config.ts to restrict framing to only required, trusted embed origins (e.g., list explicit domains like https://www.youtube.com, https://open.spotify.com) or remove the frame-src directive to fall back to default-src 'self' if framing isn't needed; modify the value string for the "Content-Security-Policy" header accordingly and ensure any required third-party embeds are enumerated rather than using a wildcard.packages/api/src/routers/backup.ts (1)
163-164: Indentation inconsistency on Line 163.The
await logAuditcall appears to have incorrect indentation (single tab instead of matching the surrounding code's double-tab level).🔧 Proposed fix
- await logAudit("backup.import", undefined, undefined, { mode }); + await logAudit("backup.import", undefined, undefined, { mode });🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/api/src/routers/backup.ts` around lines 163 - 164, Adjust the indentation of the await logAudit call so it matches the surrounding block (align with the subsequent return { success: true };), i.e., move the await logAudit("backup.import", ...) line to the same indentation level as the return statement within the same function/block to fix the inconsistent single-tab indentation.packages/api/src/routers/contacts.ts (1)
76-76: Audit logging after deletion is correctly implemented.The audit call captures the deleted contact ID for traceability. However, since
logAuditis awaited, any database failure during audit logging would cause the mutation to fail after the contact has already been deleted—the user would see an error even though the deletion succeeded.Consider whether audit logging failures should be silent (fire-and-forget) or if you want the current behavior where audit failures propagate.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/api/src/routers/contacts.ts` at line 76, The current await logAudit("contact.delete", "contact", input.id) causes the mutation to fail if audit logging errors after the contact is already deleted; change this to fire-and-forget by removing the await and ensuring failures are swallowed or logged asynchronously (e.g., call logAudit(...).catch(err => processLogger.error(...))) so the delete operation returns success even when audit logging fails; update the call site in the delete contact resolver where logAudit is invoked to use the non-blocking pattern and add a concise error log on catch.apps/web/src/app/page.tsx (1)
20-27: MissingtrackViewin useEffect dependency array.The
useEffectusestrackView.mutatebuttrackViewis not in the dependency array. While this may work due to React Query's stable mutation references, the exhaustive-deps lint rule would flag this. However, adding it could cause re-triggers if the mutation object identity changes.Consider using
trackView.mutatedirectly from the mutation result or wrapping in a ref to satisfy the lint rule without changing behavior.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/web/src/app/page.tsx` around lines 20 - 27, The effect calls trackView.mutate but doesn't include it in the dependency array; to satisfy exhaustive-deps without changing behavior, pull the mutate function out of the mutation object and reference that stable value in the effect (e.g., const { mutate } = trackView or store mutate in a ref) and then use mutate inside useEffect with mutate included in the dependency array alongside pageData.data?.profile and hasAnalyticsConsent(); update references to trackView.mutate accordingly so the lint rule is satisfied and the effect behavior remains unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@apps/web/next.config.ts`:
- Around line 23-26: The Content-Security-Policy header defined in the object
with key "Content-Security-Policy" currently sets connect-src to "'self' https:"
which will block local HTTP API calls (e.g.,
NEXT_PUBLIC_SERVER_URL=http://localhost:3000) used by the tRPC client and the
auth client's baseURL during development; update the CSP application to allow
HTTP localhost during development by adding an entry like "http://localhost" (or
the exact origin such as "http://localhost:3000") to connect-src when
process.env.NODE_ENV !== 'production', or alternatively apply the CSP header
only in production so that the connect-src directive permits development HTTP
requests used by tRPC and the auth client.
In `@apps/web/src/components/public/connect-block.tsx`:
- Around line 439-440: The consent error id is hard-coded as "consent-error" in
the ConnectBlock component (used where aria-describedby={errors.consent ?
"consent-error" : undefined} and on the visible error element), which will
collide if multiple blocks render; change to a per-instance unique id (e.g.,
derive via React's useId() or a generated uuid) and use that unique id in both
the aria-describedby attribute and the error element's id so each ConnectBlock
instance references its own error node consistently (update the JSX where
aria-describedby and the consent error element are rendered to use the generated
uniqueId).
In `@apps/web/src/components/public/consent-banner.tsx`:
- Around line 7-9: Wrap all direct localStorage access in try/catch and fall
back to safe defaults to prevent SecurityError in restricted contexts: update
hasAnalyticsConsent() to first check typeof window !== "undefined" and then
attempt localStorage.getItem(CONSENT_KEY) inside a try block, returning true
only if it equals "accepted" and returning false on any thrown error; apply the
same pattern to any other functions in this file that call localStorage.setItem
or getItem (e.g., consent setter/clearer) so they catch errors and no-op or use
in-memory fallback state instead of letting exceptions bubble.
In `@apps/web/src/middleware.ts`:
- Around line 14-15: The allowlist check using PUBLIC_ADMIN_ROUTES with
pathname.startsWith(...) is too permissive and allows prefix matches like
"/admin/login-any"; update the middleware logic (the block using
PUBLIC_ADMIN_ROUTES and pathname.startsWith) to only permit exact route matches
or route-prefixes that are followed by a path separator (e.g., check pathname
=== route || pathname.startsWith(route + "/")) before returning
NextResponse.next(), so only the intended admin paths (/admin/login,
/admin/setup and their proper subpaths) are allowed through.
In `@docker-compose.yml`:
- Line 17: The healthcheck uses curl (test: ["CMD", "curl", "-f",
"http://localhost:3000"]) but the oven/bun:latest image doesn't include curl;
either modify the Dockerfile that builds the image to install curl (add the
appropriate package install step for the base image) or change the healthcheck
to use a tool already available in the image (or a small Node.js one-liner) to
probe http://localhost:3000; update the healthcheck entry (the test array)
accordingly so probes succeed.
In `@packages/api/src/routers/public.ts`:
- Line 130: The request schema requires consent via "consent: z.literal(true, {
error: 'Consent is required' })" but the created contactSubmission record does
not persist consent; update the API to save audit fields and DB schema: add
boolean consentGiven and timestamp consentGivenAt to the contactSubmission table
via a migration, then set consentGiven = true and consentGivenAt = new Date()
when inserting the record in the router handler that constructs the
contactSubmission (update the insert/create code path referenced in this file
and the related block around lines 186-194); ensure the TypeScript types/ORM
model for contactSubmission are updated accordingly.
In `@packages/api/src/utils/sanitize.ts`:
- Around line 27-30: The browser UA parsing in sanitize.ts uses ua.match(...)
and currently maps "OPR" to "Opera" but misses modern Edge tokens like "Edg"
(and possibly "EdgA"/"EdgiOS"); update the regex used in ua.match to include the
"Edg" token (e.g., add Edg|EdgA|EdgiOS) and then extend the browser mapping
logic (the browser variable assignment that checks match[1]) to normalize any
"Edg" variants to "Edge" (in addition to the existing "OPR" => "Opera" handling)
so Edge UAs are correctly labeled as "Edge/<major>".
In `@packages/db/src/schema/audit-log.ts`:
- Around line 4-19: You added the new audit_log table schema (auditLog via
sqliteTable with columns like id, action, resourceType, resourceId, details and
createdAt) but did not generate the migration; run the migration generator (bun
run db:generate) to produce a new migration file (0005_*.sql) containing the
CREATE TABLE audit_log statement and commit that generated SQL alongside your
schema change so the deployment includes the new table (ensure the createdAt
default using unixepoch('subsecond') is preserved in the generated SQL).
---
Outside diff comments:
In `@packages/api/src/routers/public.ts`:
- Around line 121-131: Replace the inline Zod schema used in the submitContact
mutation with the shared schema from packages/validators: import the shared
contact/form schema (e.g., ContactFormSchema or SubmitContactSchema) and use it
in the mutation input instead of the current z.object({...}) block; ensure the
shared schema includes the same fields (firstName, lastName, email, whereMet,
message?, captchaToken?, blockId?, blockTitle?, consent literal true) and update
the import at the top of the file while removing the inline schema to keep
server/client validation consistent.
In `@scripts/reset-password.ts`:
- Around line 34-42: The command construction using a shell-interpolated string
(variables sql, cmd) and execSync allows shell injection via email (even if
escaped for SQL); replace the execSync call with a safe
execFileSync/child_process.spawnSync usage that passes the wrangler binary and
its args as an array (avoid any single combined shell string), build the SQL
string separately (using escapedHash/escapedEmail) but pass it as a single
argument like ["d1","execute","linkden-db","--command", sql, ...(remote ?
["--remote"] : [])] to prevent shell interpretation, and update the import/usage
from execSync to execFileSync/ spawnSync accordingly so no shell interpolation
occurs.
---
Nitpick comments:
In @.github/workflows/ci.yml:
- Around line 23-25: The "Audit dependencies" CI step currently runs "npm audit
--audit-level=moderate" with "continue-on-error: true", making vulnerability
findings non-blocking; change this so audits fail the job on protected branches
by removing or setting "continue-on-error" to false for that step (or add a
branch conditional that enforces failure when github.ref matches protected
branches) and ensure the step continues to use "npm audit
--audit-level=moderate" (or raise the audit level if desired) so moderate+
findings will block merges; update the step named "Audit dependencies" to
enforce failure accordingly.
In `@apps/web/next.config.ts`:
- Around line 23-26: The CSP entry with key "Content-Security-Policy" currently
uses the very permissive directive frame-src *; update this policy in
next.config.ts to restrict framing to only required, trusted embed origins
(e.g., list explicit domains like https://www.youtube.com,
https://open.spotify.com) or remove the frame-src directive to fall back to
default-src 'self' if framing isn't needed; modify the value string for the
"Content-Security-Policy" header accordingly and ensure any required third-party
embeds are enumerated rather than using a wildcard.
In `@apps/web/src/app/page.tsx`:
- Around line 20-27: The effect calls trackView.mutate but doesn't include it in
the dependency array; to satisfy exhaustive-deps without changing behavior, pull
the mutate function out of the mutation object and reference that stable value
in the effect (e.g., const { mutate } = trackView or store mutate in a ref) and
then use mutate inside useEffect with mutate included in the dependency array
alongside pageData.data?.profile and hasAnalyticsConsent(); update references to
trackView.mutate accordingly so the lint rule is satisfied and the effect
behavior remains unchanged.
In `@packages/api/src/routers/backup.ts`:
- Around line 163-164: Adjust the indentation of the await logAudit call so it
matches the surrounding block (align with the subsequent return { success: true
};), i.e., move the await logAudit("backup.import", ...) line to the same
indentation level as the return statement within the same function/block to fix
the inconsistent single-tab indentation.
In `@packages/api/src/routers/contacts.ts`:
- Line 76: The current await logAudit("contact.delete", "contact", input.id)
causes the mutation to fail if audit logging errors after the contact is already
deleted; change this to fire-and-forget by removing the await and ensuring
failures are swallowed or logged asynchronously (e.g., call
logAudit(...).catch(err => processLogger.error(...))) so the delete operation
returns success even when audit logging fails; update the call site in the
delete contact resolver where logAudit is invoked to use the non-blocking
pattern and add a concise error log on catch.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 67a7e019-4f38-4cf1-acd9-a4fee4745db9
📒 Files selected for processing (21)
.github/workflows/ci.ymlapps/server/src/index.tsapps/web/next.config.tsapps/web/src/app/page.tsxapps/web/src/components/public/connect-block.tsxapps/web/src/components/public/consent-banner.tsxapps/web/src/components/public/public-page-content.tsxapps/web/src/components/public/public-page.tsxapps/web/src/middleware.tsdocker-compose.ymlpackages/api/src/routers/analytics.tspackages/api/src/routers/backup.tspackages/api/src/routers/contacts.tspackages/api/src/routers/public.tspackages/api/src/routers/settings.tspackages/api/src/routers/wallet.tspackages/api/src/utils/audit.tspackages/api/src/utils/sanitize.tspackages/db/src/schema/audit-log.tspackages/db/src/schema/index.tsscripts/reset-password.ts
| aria-describedby={errors.consent ? "consent-error" : undefined} | ||
| /> |
There was a problem hiding this comment.
Use a unique consent error id per block instance.
Line 439/Line 446 use a static "consent-error" id, which can duplicate if multiple connect blocks are rendered.
💡 Suggested fix
- aria-describedby={errors.consent ? "consent-error" : undefined}
+ aria-describedby={errors.consent ? `consent-error-${blockId}` : undefined}
@@
- {errors.consent && (<span id="consent-error" className="text-xs text-red-400" role="alert">{errors.consent}</span>)}
+ {errors.consent && (
+ <span id={`consent-error-${blockId}`} className="text-xs text-red-400" role="alert">
+ {errors.consent}
+ </span>
+ )}Also applies to: 446-447
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/web/src/components/public/connect-block.tsx` around lines 439 - 440, The
consent error id is hard-coded as "consent-error" in the ConnectBlock component
(used where aria-describedby={errors.consent ? "consent-error" : undefined} and
on the visible error element), which will collide if multiple blocks render;
change to a per-instance unique id (e.g., derive via React's useId() or a
generated uuid) and use that unique id in both the aria-describedby attribute
and the error element's id so each ConnectBlock instance references its own
error node consistently (update the JSX where aria-describedby and the consent
error element are rendered to use the generated uniqueId).
| captchaToken: z.string().max(4096).optional(), | ||
| blockId: z.string().max(100).optional(), | ||
| blockTitle: z.string().max(200).optional(), | ||
| consent: z.literal(true, { error: "Consent is required" }), |
There was a problem hiding this comment.
Consent is enforced but not persisted for auditability.
consent: true is required at input, but the inserted contactSubmission record stores no consent flag/timestamp. That creates a compliance evidence gap.
If you want, I can draft the DB migration + router update (consentGiven/consentGivenAt) in a follow-up patch.
Also applies to: 186-194
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/api/src/routers/public.ts` at line 130, The request schema requires
consent via "consent: z.literal(true, { error: 'Consent is required' })" but the
created contactSubmission record does not persist consent; update the API to
save audit fields and DB schema: add boolean consentGiven and timestamp
consentGivenAt to the contactSubmission table via a migration, then set
consentGiven = true and consentGivenAt = new Date() when inserting the record in
the router handler that constructs the contactSubmission (update the
insert/create code path referenced in this file and the related block around
lines 186-194); ensure the TypeScript types/ORM model for contactSubmission are
updated accordingly.
| export const auditLog = sqliteTable( | ||
| "audit_log", | ||
| { | ||
| id: text("id").primaryKey(), | ||
| action: text("action").notNull(), | ||
| resourceType: text("resource_type"), | ||
| resourceId: text("resource_id"), | ||
| details: text("details"), // JSON string | ||
| createdAt: integer("created_at", { mode: "timestamp_ms" }) | ||
| .default(sql`(cast(unixepoch('subsecond') * 1000 as integer))`) | ||
| .notNull(), | ||
| }, | ||
| (table) => [ | ||
| index("audit_log_created_at_idx").on(table.createdAt), | ||
| index("audit_log_action_idx").on(table.action), | ||
| ], |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Check if audit-log migration exists
echo "=== Looking for audit_log migration files ==="
fd -e sql . packages/db/drizzle --exec grep -l "audit_log" {} \; 2>/dev/null || echo "No migration files found matching audit_log"
# Check all migration files
echo ""
echo "=== All migration files ==="
fd -e sql . packages/db/drizzle 2>/dev/null || fd -e sql . packages/db/migrations 2>/dev/null || echo "No migrations directory found"Repository: MrDemonWolf/linkden
Length of output: 211
🏁 Script executed:
# Check the packages/db directory structure
echo "=== packages/db directory structure ==="
fd . packages/db --type f --max-depth 3 2>/dev/null | head -30
# Look for Drizzle configuration files
echo ""
echo "=== Looking for Drizzle/database config files ==="
fd -e config -e conf -e setup . packages/db 2>/dev/null || echo "No config files found"
fd "drizzle.config" . 2>/dev/null || echo "No drizzle.config found"
fd "package.json" packages/db 2>/dev/null
# Look for all .sql files anywhere in the repo
echo ""
echo "=== All .sql files in repo ==="
fd -e sql . 2>/dev/null || echo "No .sql files found"Repository: MrDemonWolf/linkden
Length of output: 1110
🏁 Script executed:
# Check if audit-log is exported in schema/index.ts
echo "=== packages/db/src/schema/index.ts content ==="
cat packages/db/src/schema/index.ts
# Check the latest migration to see if audit_log is already included
echo ""
echo "=== Latest migration (0004_legal_loners.sql) ==="
cat packages/db/src/migrations/0004_legal_loners.sql
# Look for any documentation about SQLite version or D1 requirements
echo ""
echo "=== Looking for SQLite/D1 version documentation ==="
fd -e md . 2>/dev/null | xargs grep -l "SQLite\|D1\|3.42\|unixepoch" 2>/dev/null | head -5Repository: MrDemonWolf/linkden
Length of output: 649
🏁 Script executed:
# Check CLAUDE.md, GEMINI.md, README.md for SQLite/D1 version info
echo "=== CLAUDE.md (SQLite/D1 info) ==="
grep -A 5 -B 5 "SQLite\|D1\|3.42\|unixepoch" CLAUDE.md | head -40
echo ""
echo "=== GEMINI.md (SQLite/D1 info) ==="
grep -A 5 -B 5 "SQLite\|D1\|3.42\|unixepoch" GEMINI.md | head -40
echo ""
echo "=== README.md (SQLite/D1 info) ==="
grep -A 5 -B 5 "SQLite\|D1\|3.42\|unixepoch" README.md | head -40Repository: MrDemonWolf/linkden
Length of output: 3897
🏁 Script executed:
# Check drizzle.config.ts for any settings
echo "=== packages/db/drizzle.config.ts ==="
cat packages/db/drizzle.config.ts
# Check package.json for Drizzle and SQLite versions
echo ""
echo "=== packages/db/package.json (relevant sections) ==="
grep -A 10 "drizzle\|sqlite" packages/db/package.jsonRepository: MrDemonWolf/linkden
Length of output: 878
🌐 Web query:
Cloudflare D1 SQLite version support unixepoch subsecond
💡 Result:
Cloudflare D1 supports the SQLite unixepoch function, including subsecond precision via floating-point timestamps. D1 is compatible with most of SQLite's SQL conventions and leverages SQLite's query engine, which includes the full date and time functions like unixepoch. Official docs confirm support for SQLite extensions and PRAGMA statements, and examples use date(timestamp, 'unixepoch') in generated columns, confirming unixepoch modifier availability. unixepoch returns seconds since 1970-01-01 00:00:00 UTC as an integer by default, but accepts floating-point inputs for subsecond (fractional seconds) precision, as per SQLite behavior adopted by D1. No sources indicate limitations on subsecond support. To check exact SQLite version: execute PRAGMA sqlite_version; in D1 console (supported). Recent docs (2025-2026) show ongoing compatibility.
Citations:
- 1: https://developers.cloudflare.com/d1/sql-api/sql-statements
- 2: https://developers.cloudflare.com/d1/reference/generated-columns
- 3: https://sqlite.org/matrix/lang_datefunc.html
- 4: [D1] How to store and query Date and Time values cloudflare/cloudflare-docs#15093
Run bun run db:generate to create the migration file for the audit_log table.
The schema is correctly defined with Drizzle ORM and properly exported from packages/db/src/schema/index.ts, but the migration file has not been generated. The latest migration is 0004_legal_loners.sql; a 0005_*.sql file containing the CREATE TABLE audit_log statement must be generated before deployment. Per coding guidelines, after schema changes you must run bun run db:generate and commit the resulting migration file.
The unixepoch('subsecond') syntax is supported by Cloudflare D1.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/db/src/schema/audit-log.ts` around lines 4 - 19, You added the new
audit_log table schema (auditLog via sqliteTable with columns like id, action,
resourceType, resourceId, details and createdAt) but did not generate the
migration; run the migration generator (bun run db:generate) to produce a new
migration file (0005_*.sql) containing the CREATE TABLE audit_log statement and
commit that generated SQL alongside your schema change so the deployment
includes the new table (ensure the createdAt default using
unixepoch('subsecond') is preserved in the generated SQL).
Addresses CodeRabbit docstring coverage warning (was 66.67%, threshold 80%). Adds @param/@returns docs to logAudit(), hasAnalyticsConsent(), ConsentBanner(), and middleware(). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- scripts/reset-password.ts: replace execSync+shell string with execFileSync and arg array to eliminate shell injection via email metacharacters (critical) - apps/web/src/middleware.ts: tighten PUBLIC_ADMIN_ROUTES check to exact match or proper subpath (prevents /admin/login-anything bypass) - apps/web/src/components/public/consent-banner.tsx: wrap all localStorage access in try/catch to handle SecurityError in restricted browser contexts - packages/api/src/utils/sanitize.ts: add Edg/EdgA/EdgiOS tokens to UA regex so modern Edge is correctly labelled as Edge/<version> - packages/api/src/routers/backup.ts: fix indentation on logAudit call - packages/api/src/routers/contacts.ts: make logAudit fire-and-forget (void) so a logging failure cannot surface after a successful delete - apps/web/src/app/page.tsx: destructure trackViewMutate from mutation to satisfy exhaustive-deps without changing effect behaviour - apps/web/next.config.ts: allow http://localhost:* in connect-src during development so tRPC calls work; keep frame-src * (intentional for embeds) - docker-compose.yml: replace curl (not in bun image) with wget for healthcheck - packages/db/src/migrations/0005_talented_shotgun.sql: generated migration for audit_log table Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
match[1] and match[2] are typed as string | undefined in strict mode even inside an if(match) block; use optional chaining to satisfy tsc. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Summary
Addresses all code-addressable findings from the GDPR and ISO 27001:2022 gap analyses. Scoped appropriately for a self-hosted single-user deployment.
/admin/*routessettings.getnow masks secrets consistently withgetAllaudit_logtable +logAudit()utility wired into settings updates, backup import/export, and contact deletionspurgeExpiredDataprocedure added to analytics router (90-day cutoff for page views and link clicks)CHANGE-ME-BEFORE-DEPLOYreset-password.tsscriptNew files
apps/web/src/middleware.ts— Next.js edge auth middlewareapps/web/src/components/public/consent-banner.tsx— consent banner +hasAnalyticsConsent()helperpackages/api/src/utils/audit.ts—logAudit()utilitypackages/db/src/schema/audit-log.ts— audit log DB tableTest plan
/admin/*routes redirect to/admin/loginwhen unauthenticatedsettings.getfor secret keys returns••••••getConfigmasks cert/key fieldsbun run check-typespasses ✅🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Security
Chores