URL: https://mkdocs.justinsforge.com/memory/handoffs/creatortrack-overnight-audit-2026-07-09/
CreatorTrack Overnight Audit, 2026-07-09/10¶
Where the work lives¶
Branch audit/overnight-0709, worktree /home/justinwieb/forge-suite-wt-manual/audit-overnight-0709. Two commits sit on top of main, nothing pushed, nothing merged, main checkout untouched:
The workflow ran as a fanout job (wf_369c8431-8c3): a wave of finder agents scans a zone, a verifier agent confirms or rejects each finding as a real reproducible bug, a fixer agent patches the confirmed ones inside its owned files, and a gate agent typechecks/lints/commits. Nothing gets committed without gate sign-off.
Scope¶
All API routes under app/api/** plus their backing lib/*.ts modules, split into 12 zones (attachments/files, collection/database, chat, tasks, projects, finance (jobs/recurring/icons/categorize), calendar sync (push+pull), google-tasks-sync, knowledge, crm/campaigns, workspace-transfer/onboarding, presence/notifications/kit-tokens/misc). Five audit dimensions per zone:
- Correctness (logic bugs, dead branches, wrong assumptions)
- Workspace-scoping / auth (cross-tenant leakage, missing RLS/membership checks)
- Races (unlocked read-modify-write, concurrent double-submit)
- Fail-loud (swallowed errors, silent-success on failure)
kind:"project"pattern (rows that should carry the Projects-app discriminator but don't, breaking Kind-filtered queries)
Round 1: confirmed fixes (commit 1c0517e)¶
34 files, +934/-291. Gate: PASSED, committed as 1c0517e0b162f3e5128f91b936466b362c1698ec. Typecheck: 6 pre-existing errors, all in unmodified marketing/layout files (missing generated PNG-module/LayoutProps types, needs a real next build), zero in the 32 touched TS files. Lint: 462 pre-existing repo-wide problems (baseline), zero in the modified files.
Representative confirmed bugs by dimension:
- Fail-loud:
lib/campaigns-jobs.tscatch swallowed job failures by letting arecordRunDB error replace the original error and skipnotifyFailure; nownotifyFailurefires first,recordRungets its own try/catch with aWhy swallowedcomment, original error always rethrown. - Workspace-scoping/auth:
lib/chat.ts(many tool handlers,update_task/append_page/insert_block/rename_page/set_page_icon/delete_page/insert_entity_link) were stampingrecordAiPageEdit/write calls with the chat session's workspace id instead of the target node's own workspace id; now resolved offNumber(node.workspaceId)per the already-confirmededit_block/delete_blockpattern. Alsodelete_task(forever) now validates viagetTaskByIdand rejects cross-workspace ids before callingdeleteNodeForever. - kind:"project" pattern:
app/api/tasks/projects/[id]/uncheck/route.ts:lineswappedgetProjectById(drops list rows via the Kind filter) for a direct row/workspace resolution so list-type project rows resolve instead of 404ing. - Workspace-scoping/auth:
app/api/finance/categorize/rules/route.tsGET/POST were missingworkspace_id = <activeWs>on the counts query and both halves of the rules union/delete/toggle; now scoped viaresolveActiveFinanceWorkspace. - Races:
lib/calendar-sync-push.tspushOnehad no lock; overlapping fire-and-forget pushes created duplicate orphaned Google events. Now wraps in oneasUsertx that takespg_advisory_xact_lock(hashtext('cal-sync:<ws>:<kind>'), hashtext(ctId))before read/create/patch/upsert. - Correctness:
lib/calendar-sync-push.tsall-day exclusive end date was computed as start+24h instead of advancing the date string, breaking on DST fall-back (empty range 400). - Workspace-scoping/auth:
lib/clips.ts(clipFootage/clipPoster/keyframeImage) gained an optionalpinWorkspaceId, wired through the Demo wrappers, so demo-owner elevation can no longer reach other workspaces' rows. - Correctness:
app/api/tasks/settings/route.tswas missing"projectRel"from its localGROUP_BYSwhitelist, silently dropping that groupBy patch. - Correctness/fail-loud:
app/api/kit/tokens/route.tstoken-save regex had no theme awareness and nogflag, always rewriting only the dark:rootdeclaration even when the caller was editing light mode, and reporting{ok:true}regardless. Now theme-aware, 422s instead of silently corrupting on ambiguous dual-declared tokens. - Races:
app/api/chat/route.tsschedulePersist's queued re-fire in.finallycould race and clobber the terminal done/error write; added afinalizedflag plus adrainPersist()helper awaited before both terminal writes. - Fail-loud:
app/api/footage/[id]/play/route.ts(and the sibling review-token route fixed in round 2) decided stream success/failure offstdout "end"instead of ffmpeg's exit code, so a crashed transcode still returned a clean 200 with a truncated file; now decides onclose(code)with a bounded 4KB stderr tail surfaced on failure. - Correctness:
lib/workspace-transfer.tstransferProjectdidn't passkind: "project"tocreateProject, orphaning the destination row past the Kind filter;requireMembershipdidn't reject archived/trashed workspaces as transfer targets. - Workspace-scoping/auth:
lib/finance-recurring.tsmigrated wholesale fromasUsertoasFinanceUserwith active-workspace scoping added to every finance.* query (reads, edits, deletes,addRecurring's account/history/series lookups); cross-workspace recId/merchantKey/accountId rewrites are no longer possible. - Correctness:
lib/google-contacts-sync.tspersisted the PeoplenextSyncTokeneven when the pull loop had per-item errors, silently dropping failed contacts forever; token now held back on any failure so they re-pull next run. - Workspace-scoping/auth:
lib/finance-jobs.tsownerWorkspaceId()(lowest-id membership, archived included) replaced withresolveActiveFinanceWorkspace(me), the same resolver the finance read layer uses, so Sync/rescan/initial-pull target the workspace the caller is actually viewing. - kind:"project" pattern / correctness:
db/migrations/20260710T134604_..._calendar_wsgoogle_checks_and_per_user_sync_keys.sqlwidenssource_accountCHECKs to acceptwsgoogle:%and re-keys both unique constraints onuser_id(not applied to any DB; deploy does not auto-run migrations). - Workspace-scoping/auth:
lib/finance-jobs/holdings.tsgated the global Fidelity inbox scan/import/archive to only the CAPTURE_EMAIL finance owner's first-membership workspace; other fan-out workspaces get price/constituent refresh only, killing a cross-tenant CSV import + file-loss path. - Races:
lib/google-tasks-sync.tsinbound sync advanced its cursor even when an item failed to apply, permanently skipping it; now aninboundFailedflag holds the cursor back on any inbound failure. - Fail-loud:
lib/attachments.tsdeleteAttachmentused to unlink the shared content-hash blob on metadata delete, but bytes are shared across all workspaces viaapp.node_attachments/app.fileswith no refcount visibility under RLS; unlink removed entirely (orphans left to offline GC), metadata delete unchanged.
Round 2: confirmed fixes (commit 5cc67c5)¶
23 files, +588/-165. Gate: PASSED, committed as 5cc67c51e497788045f0370a62111ead9fcca676. Typecheck: same 6 pre-existing errors, none in the 23 touched files. Lint: 2 findings inside changed files, both independently verified as pre-existing in base commit 37d2a87 (identical lines, outside the audit's diff hunks): lib/projects.ts:417 prefer-const and lib/tasks.ts:11 unused import; neither introduced by the fixers.
Representative confirmed bugs by dimension:
- Correctness/kind:"project" pattern:
app/api/collection/prop/[id]/duplicate/route.tstrusted the body'sworkspaceId/nodeIdwhile copying cell values by sourcepropIdalone, letting a propId from another database graft a foreign column and cross-workspace orphan rows into the target db. Now requires the source prop to actually live in the claimed database first. - Workspace-scoping/auth:
app/api/collection/prop/route.tsaccepted a client-suppliedworkspaceIdthat could be stale or point at a different workspace the caller also belonged to, minting props whoseworkspace_iddisagreed with their node's (invisible to RLS reads). Now resolvesworkspaceIdfrom the database node itself. - Workspace-scoping/auth:
app/api/collection/relation/route.tslet any member link arbitrary visible rows through any propId, corrupting rollups and mis-scoping the relation row under a caller-picked workspaceId. Now validates the propId is a relation prop onfromRow's parent collection,toRowlives in the prop's configured target db, and stamps the relation withfromRow's own workspace. - Correctness/fail-loud:
app/api/files/upload/route.tsNumber(null)/Number("")both coerce to0, slipping past the integer check, writing the blob, then dying as a 500 on the RLS insert. Raw form field now validated before coercion (rejects missing/empty/non-integer/<=0). - Correctness:
app/api/fitness/detail/exercise/[name]/route.tsdouble-decodeURIComponent'd a route param Next.js already decodes, throwing on titles with a literal%(e.g. "Squat 80%"). - kind:"project" pattern:
app/api/projects/[id]/route.tsPATCH/DELETE now callgetProjectById(me, rowId, { includeArchived: true })so restore ({archived:false}) finds the archived row instead of 404ing, and DELETE 404s upfront instead of archiving a task/CRM/list row then 500ing after the archive already committed. - Fail-loud:
app/api/review/[token]/play/route.tssame ffmpeg exit-code fix as round 1's footage route (mirrors it directly), so guest reviewers get a stream error instead of a silently truncated clip. - kind:"project" pattern / correctness:
app/api/tasks/[id]/route.tsgained anisTaskRowguard resolving the row against the workspace's actualtasks.dbbefore any write; previouslyupdateTask/archiveTaskwould mutate anydatabase_row(e.g. a projects-db row) and only the post-write re-read would throw, leaving the wrong row mutated under a 500. - Correctness:
app/api/projects/route.tstemplate branch calledcreateProjectFromTemplatewhose option type silently dropspriority/tags/assignedTo; now composescreateProject(full payload) thenapplyTemplateToProjectin-route. - Correctness:
app/api/kit/tokens/route.ts(round-2 half of the round-1 fix) accepts{theme}, computes light-block bounds per token, 422s cleanly on ambiguous dual-declared tokens with no theme supplied. - Correctness:
lib/workspace-transfer.tscopyTaskIntowas droppingdueEndand linking via the legacy listprojectIdinstead ofprojectRelId;transferProjecttask-matching now mirrorslib/tasks.ts's own matching logic and carriesicon/recurring/priority/tagsfrom source. - Correctness:
lib/knowledge-readlater.tsdecodeEntitiesnumeric-entity replacer had no bounds check, throwing an uncaughtRangeErroron malformed HTML entities (also coverslib/news-article.tsvia the exportedextractReadable); now bounds-checks and falls back toU+FFFD/empty string.
Fenced-off scope, owned by other live sessions¶
Not touched, not audited in this pass:
- Links / Link-In-Bio module
- Ideas-planner apps
- Admin analytics
- Performance work
These are all live under other concurrent sessions per MEMORY.md; the audit fenced them off to avoid stepping on in-flight work.
Round 3: stopped mid-find, by Justin's call¶
Round 3 started (7 finder agents dispatched, all returned) but was stopped before verification for diminishing returns against daytime usage; no verifier, fixer, or gate ran. Nothing here is confirmed real, nothing is committed, but listed below so no lead is silently dropped.
Unverified round-3 leads (raw finder output, NOT verified)¶
app/api/kit/tokens/route.ts:94[correctness] Token save 422s for every dual-declared token because the sole caller never sends the requiredthemeparam (follow-on gap from the round-1/2 fix)app/api/workspace/[id]/invites/[inviteId]/route.ts:43[workspace-scoping/auth] Invite-nudge endpoint has no cooldown: unbounded email-spam relaylib/workspace-transfer.ts:134[races]transferTask"move" has no concurrency/idempotency guard: double-submit duplicates the tasklib/workspace-transfer.ts:158[correctness]transferProjectmulti-step copy is non-transactional: mid-loop failure strands a partial copy, retry duplicateslib/onboarding.ts:67[races]patchOnboardingread-modify-write with no lock: concurrent step patches silently drop flagsapp/api/workspace/icon/route.ts:20[fail-loud] bare catch discards the error entirely, no log, no rethrowlib/calendar.ts:491[correctness] Google-to-Google calendar move is delete-then-create with no failure recovery: event destroyed if create failslib/google-tasks-sync.ts:484[correctness] Outbound sweep'slistTasks(view:'all')excludes done/archived tasks, making archived-cleanup dead codelib/calendar-sync-push.ts:479[correctness] Sameview:'all'filtering bug blocks removal of done/archived task mirrorslib/calendar.ts:356[correctness] Creating/promoting a recurring event caches the recurrence MASTER row that sync never reconciles, duplicating the first occurrencelib/google-tasks-settings.ts:93[races]setGtasksStateread-modify-write across two transactions with no lock: cursor persist can race an account switchapp/api/tasks/projects/route.ts:49[correctness] List/project creation non-atomic:createRowcommits beforeupdateProject, phantom Kind-less row on failurelib/projects.ts:363[correctness]createProjectorphans a Kind-less row when the follow-upupdateProject/reload throwsapp/api/projects/route.ts:76[correctness] Template-based project creation non-atomic/non-idempotent: project persists with partial tasks on template-apply failurelib/projects.ts:538[races]addTaskFieldduplicate-name guard is an unlocked check-then-insertapp/api/ai/generate/route.ts:101[correctness] AI writer model override validated against the legacy provider, not the one the request actually runs onlib/chat.ts:2471[workspace-scoping/auth]replace_in_page/set_page_bannerstill stamp activity/snapshot rows with the chat workspace id, not the node's own (same class as round-1 fix, these two were explicitly left untouched then as "not in the confirmed finding")app/api/chat/route.ts:302[races] No per-conversation in-flight guard: a second POST mid-turn forks history and duplicates tool mutationslib/chat.ts:1915[correctness]get_habitscomputestodayDoneagainst the full period goal, always reporting weekly-frequency habits not-donelib/chat.ts:2539[correctness]delete_pagetrashes any node type with no type check, including system app nodes and databaseslib/notifications.ts:330[workspace-scoping/auth]notifyMentionsdispatches to attacker-supplied user ids with no membership check: cross-tenant notification injectionapp/api/capture/route.ts:150[correctness] Capture "task" verb stores the bot-supplied reminder string verbatim; lexicographic compare fires naive-local reminders early or neverapp/api/files/[id]/route.ts:106[workspace-scoping/auth] Stored XSS: uploaded files served inline with client-supplied mime, no nosniffapp/api/attachments/[id]/route.ts:38[workspace-scoping/auth] Same stored-XSS vector on the attachments download routelib/media-search.ts:194[correctness]searchMediaCached's in-process cache grows unbounded, never evicted
Rejected findings (rough counts)¶
Verifier votes (not 1:1 with finding count; multiple verifier passes per finding in some rounds): round 1, 119 confirmed-real votes vs 25 rejected; round 2, 93 confirmed-real vs 12 rejected. Round 3 has zero verifier votes (stopped before verification stage).
Review commands (do not execute; for Justin's review only)¶
# Review the full round 1 + round 2 diff against main
git -C /home/justinwieb/forge-suite-wt-manual/audit-overnight-0709 diff main
# Review each round's commit individually
git -C /home/justinwieb/forge-suite-wt-manual/audit-overnight-0709 show 1c0517e
git -C /home/justinwieb/forge-suite-wt-manual/audit-overnight-0709 show 5cc67c5
# When ready to merge into main
git -C /home/justinwieb/forge-suite-wt-manual/creatortrack checkout main
git -C /home/justinwieb/forge-suite-wt-manual/creatortrack merge audit/overnight-0709
Note: git diff main --stat on the worktree shows a larger file list than the two audit commits alone (125 files, +1894/-4495) because main has moved forward independently since the branch was cut (workspace-switcher refactor, map-pins/knowledge/tasks dead-code removal, etc. from other live sessions). The audit's actual changes are exactly the two commits above; diffing directly against main..HEAD (as in the log/diff-stat at the top of this doc) isolates them from that drift, but a plain diff main will also surface main's independent moves as reverse-diffs. Rebase or a targeted three-dot diff before merging to avoid confusing the two.
[Claude Code]
URL: https://mkdocs.justinsforge.com/memory/handoffs/creatortrack-overnight-audit-2026-07-09/