Skip to content

Harden project security, deletion, and quality gates - #68

Draft
t3dotgg wants to merge 1 commit into
mainfrom
codex/project-hardening
Draft

Harden project security, deletion, and quality gates#68
t3dotgg wants to merge 1 commit into
mainfrom
codex/project-hardening

Conversation

@t3dotgg

@t3dotgg t3dotgg commented Jul 21, 2026

Copy link
Copy Markdown
Member

Summary

  • Delete S3 objects and Mux assets through idempotent scheduled cleanup with bounded retries, and drain project/team deletion in bounded batches.
  • Move authorization and ownership checks to issuer-scoped Convex identities with dual reads/writes, a resumable production migration, and cryptographically secure invite tokens.
  • Remove the duplicate comments subscription, make comment threading linear, and keep the team landing page off the full storage-usage scan.
  • Replace blocking error alerts, enable strict null checking and GitHub Actions, add focused security/lifecycle tests, and remove unused dependencies and template assets.

Why

The audit found four material risks: deleted database rows could leave paid external assets behind, subject-only identity comparisons could collide across issuers, common routes performed redundant or amplified reads, and the repository had no enforced quality gate. This change addresses those findings while keeping the identity rollout backwards-compatible with existing data.

Deployment impact

Deploy the widened schema and dual-read code first, then run the documented resumable identity migration in docs/deployment.md. Legacy identity fields remain in place for rollback and should only be narrowed in a later deployment after every migration reports done.

Validation

  • bun install --frozen-lockfile
  • bun run check (52 unit tests and 44 Convex tests)
  • bun run build

Note

Harden project security, deletion, and quality gates with canonical identity and batched cleanup

  • Introduces a canonical tokenIdentifier-based identity system across teams, members, invites, videos, comments, and share links, replacing legacy Clerk subject comparisons in all authorization guards (requireTeamAccess, requireProjectAccess, requireVideoAccess) and mutation handlers.
  • Adds backfill migrations (convex/migrations.ts) to populate new userIdentity/ownerIdentity/uploadedByIdentity/createdByIdentity fields on existing records using the legacy issuer+subject format.
  • Replaces in-transaction cascade deletes for teams with a batched async workflow (deleteTeamBatch + continueTeamDelete) and adds a deleteVideoAssets internal action with exponential backoff retries to clean up S3 and Mux resources on video deletion.
  • Switches the video page to use only threaded comments from getThreaded, flattens them locally for counts and player markers, and removes the comments:list query; also replaces window.alert calls with inline error notices throughout the dashboard.
  • Enables strictNullChecks in tsconfig.json and adds a GitHub Actions CI workflow that runs bun run check on every push and pull request.
  • Risk: the by_video_and_timestamp comments index is removed from the schema and is no longer available at runtime.
📊 Macroscope summarized 332bbee. 33 files reviewed, 0 issues evaluated, 0 issues filtered, 0 comments posted

🗂️ Filtered Issues

No issues evaluated.

@vercel

vercel Bot commented Jul 21, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
lawn Ready Ready Preview, Comment Jul 21, 2026 12:18am

Request Review

@coderabbitai

coderabbitai Bot commented Jul 21, 2026

Copy link
Copy Markdown

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 2ee3cc4c-6960-47f8-94cf-d74d1a81e3ed

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/project-hardening

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

Comment thread convex/teams.ts

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Medium

const team = await ctx.db.get(teamId);

continueTeamDelete resumes team deletion across multiple transactions without rechecking getTeamSubscriptionState. If a checkout or webhook activates the team's paid subscription after the initial guard in deleteTeam but before a continuation runs, deleteTeamBatch still deletes the team record — leaving an active paid subscription with no team. Recheck getTeamSubscriptionState at the start of deleteTeamBatch (or set a deleting flag on the team and block subscription activation once deletion starts).

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @convex/teams.ts around line 440:

`continueTeamDelete` resumes team deletion across multiple transactions without rechecking `getTeamSubscriptionState`. If a checkout or webhook activates the team's paid subscription after the initial guard in `deleteTeam` but before a continuation runs, `deleteTeamBatch` still deletes the team record — leaving an active paid subscription with no team. Recheck `getTeamSubscriptionState` at the start of `deleteTeamBatch` (or set a `deleting` flag on the team and block subscription activation once deletion starts).

}, [reconcileTeamSubscription, team]);

if (context === undefined || shouldCanonicalize) {
if (context === undefined || team === undefined || shouldCanonicalize) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟠 High dashboard/-settings.tsx:113

When resolveContext returns null for an unknown or unauthorized team, context is null and team (derived from context?.team) is undefined. The first early-return guard checks team === undefined, so it matches this completed not-found state and renders "Loading..." forever — the subsequent context === null || !team branch that should show "Team not found" never runs. Move the team === undefined check into the not-found branch instead of the loading branch.

Suggested change
if (context === undefined || team === undefined || shouldCanonicalize) {
if (context === undefined || shouldCanonicalize) {
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @app/routes/dashboard/-settings.tsx around line 113:

When `resolveContext` returns `null` for an unknown or unauthorized team, `context` is `null` and `team` (derived from `context?.team`) is `undefined`. The first early-return guard checks `team === undefined`, so it matches this completed not-found state and renders "Loading..." forever — the subsequent `context === null || !team` branch that should show "Team not found" never runs. Move the `team === undefined` check into the not-found branch instead of the loading branch.

? uploadableProjectIds.has(routeProjectId)
: false;

useEffect(() => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Medium dashboard/-layout.tsx:88

When the same drop error is triggered twice in succession, the second occurrence disappears almost immediately instead of staying visible for 4 seconds. setDropError receives the same string value that is already in state, so React skips the re-render and the useEffect keyed on dropError never restarts. The original 4-second timeout continues counting down from the first occurrence and clears the second error as little as 0.1 seconds after it appears. Consider keying the timer on a counter or error id so repeated identical errors reset the dismissal window.

Also found in 1 other location(s)

app/routes/dashboard/-project.tsx:275

handleCreateFolder reuses dndError, whose dismissal timer only restarts when the string value changes. If folder creation fails, the user retries within three seconds, and the retry produces the same error message, React keeps the same state value and the existing timer is not reset; the newly surfaced failure can therefore disappear almost immediately instead of remaining visible for a full dismissal interval.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @app/routes/dashboard/-layout.tsx around line 88:

When the same drop error is triggered twice in succession, the second occurrence disappears almost immediately instead of staying visible for 4 seconds. `setDropError` receives the same string value that is already in state, so React skips the re-render and the `useEffect` keyed on `dropError` never restarts. The original 4-second timeout continues counting down from the first occurrence and clears the second error as little as 0.1 seconds after it appears. Consider keying the timer on a counter or error id so repeated identical errors reset the dismissal window.

Also found in 1 other location(s):
- app/routes/dashboard/-project.tsx:275 -- `handleCreateFolder` reuses `dndError`, whose dismissal timer only restarts when the string value changes. If folder creation fails, the user retries within three seconds, and the retry produces the same error message, React keeps the same state value and the existing timer is not reset; the newly surfaced failure can therefore disappear almost immediately instead of remaining visible for a full dismissal interval.

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.

1 participant