Skip to content

[codex] Harden CLI auth and durable rate limits#130

Open
ohong wants to merge 3 commits into
mainfrom
codex/security-sweep
Open

[codex] Harden CLI auth and durable rate limits#130
ohong wants to merge 3 commits into
mainfrom
codex/security-sweep

Conversation

@ohong

@ohong ohong commented Jun 9, 2026

Copy link
Copy Markdown
Owner

Summary

This PR implements the coordinated security sweep for the CLI login flow, public database exposure, durable abuse limits, expensive request hardening, AI caption image validation, and vulnerable dependency paths.

What changed

  • Hardened CLI device auth by splitting the human code, browser-only verify secret, and CLI-only poll secret.
  • Added one-time redemption for completed CLI auth codes and removed direct public client access to public.cli_auth_codes through a Supabase migration.
  • Replaced the in-memory API limiter with a Supabase-backed api_rate_limits table and atomic check_rate_limit RPC.
  • Added durable limits for CLI init, uploads, usage submits, social actions, messages, and AI captions.
  • Capped usage submit request bodies at 256 KB, rejected duplicate dates and over-32-entry batches, and bounded per-entry processing concurrency to 4 while preserving response ordering.
  • Restricted AI caption image inputs to first-party public post-images storage URLs.
  • Updated vulnerable dependency paths for Supabase, PostHog, Vitest/Vite, Turbo, Resend, and related transitive overrides.

Impact

Existing CLI login clients that only send { code } to /api/auth/cli/poll are intentionally no longer supported. Current CLI login stores the new poll_secret in memory and includes it on every poll.

Root Cause

The previous device-login code was redeemable by code alone, and cli_auth_codes still had public client access paths. Rate limiting was also process-local, so it did not survive serverless instance churn. Several high-cost routes could do too much work before durable abuse checks or strict input caps.

Validation

  • bun install --frozen-lockfile
  • bun audit --json
  • bun run typecheck
  • bun run lint
  • bun run test
  • focused web security tests: usage submit, CLI auth, AI caption, rate limit, migration safety
  • focused CLI tests: login and sync flow
  • migration smoke-tested against local Supabase Postgres inside BEGIN ... ROLLBACK, including a check_rate_limit first-call allowed / second-call denied check
  • git diff --check

Notes

bun --cwd apps/web test:integration was not run locally because the current local Supabase migration history contains applied migration 20260602000000, but that migration file is not present in this checkout. I did not repair local migration history automatically.

Summary by CodeRabbit

  • New Features

    • Durable, server-backed rate limiting across APIs.
    • Per-user AI caption quotas and stricter AI caption flow.
  • Security & Validation

    • Stronger CLI device-secret verification and tightened CLI auth flows.
    • Image uploads restricted to first-party post-image storage.
    • Request body size, entry-count, and duplicate-date validation for submissions.
  • Tests

    • Expanded tests covering rate limits, CLI flows, uploads, and edge cases.
  • Chores

    • Dependency version bumps and a database migration for rate limits and CLI hardening.

@vercel

vercel Bot commented Jun 9, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
straude Ready Ready Preview, Comment Jun 13, 2026 7:16pm

Request Review

@coderabbitai

coderabbitai Bot commented Jun 9, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 611905cf-a9dc-42d6-bf9a-b8518b4a375e

📥 Commits

Reviewing files that changed from the base of the PR and between 89143f6 and 0370aa3.

⛔ Files ignored due to path filters (1)
  • bun.lock is excluded by !**/*.lock
📒 Files selected for processing (34)
  • apps/web/__tests__/api/ai-caption.test.ts
  • apps/web/__tests__/api/auth-cli.test.ts
  • apps/web/__tests__/api/comment-email-notifications.test.ts
  • apps/web/__tests__/api/social.test.ts
  • apps/web/__tests__/api/upload.test.ts
  • apps/web/__tests__/api/usage-submit.test.ts
  • apps/web/__tests__/flows/cli-push-flow.test.ts
  • apps/web/__tests__/flows/post-lifecycle.test.ts
  • apps/web/__tests__/flows/social-interactions.test.ts
  • apps/web/__tests__/flows/web-import-flow.test.ts
  • apps/web/__tests__/unit/migration-safety.test.ts
  • apps/web/__tests__/unit/rate-limit.test.ts
  • apps/web/app/api/ai/generate-caption/route.ts
  • apps/web/app/api/auth/cli/init/route.ts
  • apps/web/app/api/auth/cli/poll/route.ts
  • apps/web/app/api/auth/cli/verify/route.ts
  • apps/web/app/api/comments/[id]/reactions/route.ts
  • apps/web/app/api/comments/[id]/route.ts
  • apps/web/app/api/follow/[username]/route.ts
  • apps/web/app/api/messages/route.ts
  • apps/web/app/api/posts/[id]/comments/route.ts
  • apps/web/app/api/posts/[id]/kudos/route.ts
  • apps/web/app/api/upload/route.ts
  • apps/web/app/api/usage/submit/route.ts
  • apps/web/app/cli/verify/page.tsx
  • apps/web/e2e/golden-path/cli-verify.spec.ts
  • apps/web/lib/api/cli-auth.ts
  • apps/web/lib/rate-limit.ts
  • apps/web/package.json
  • package.json
  • packages/cli/__tests__/commands/login.test.ts
  • packages/cli/package.json
  • packages/cli/src/commands/login.ts
  • supabase/migrations/20260609200740_security_sweep_cli_auth_rate_limits.sql
✅ Files skipped from review due to trivial changes (3)
  • packages/cli/package.json
  • apps/web/tests/api/comment-email-notifications.test.ts
  • apps/web/tests/flows/social-interactions.test.ts
🚧 Files skipped from review as they are similar to previous changes (27)
  • apps/web/tests/flows/post-lifecycle.test.ts
  • apps/web/app/api/posts/[id]/comments/route.ts
  • apps/web/app/api/messages/route.ts
  • apps/web/app/api/upload/route.ts
  • apps/web/app/api/posts/[id]/kudos/route.ts
  • apps/web/tests/unit/migration-safety.test.ts
  • apps/web/app/api/comments/[id]/reactions/route.ts
  • apps/web/tests/unit/rate-limit.test.ts
  • apps/web/app/api/comments/[id]/route.ts
  • apps/web/tests/api/upload.test.ts
  • apps/web/package.json
  • package.json
  • apps/web/tests/flows/web-import-flow.test.ts
  • apps/web/app/cli/verify/page.tsx
  • apps/web/app/api/follow/[username]/route.ts
  • apps/web/app/api/auth/cli/verify/route.ts
  • apps/web/app/api/auth/cli/init/route.ts
  • packages/cli/tests/commands/login.test.ts
  • apps/web/tests/flows/cli-push-flow.test.ts
  • packages/cli/src/commands/login.ts
  • apps/web/tests/api/usage-submit.test.ts
  • apps/web/e2e/golden-path/cli-verify.spec.ts
  • apps/web/app/api/usage/submit/route.ts
  • apps/web/tests/api/ai-caption.test.ts
  • supabase/migrations/20260609200740_security_sweep_cli_auth_rate_limits.sql
  • apps/web/app/api/auth/cli/poll/route.ts
  • apps/web/tests/api/auth-cli.test.ts

📝 Walkthrough

Walkthrough

This PR implements durable API rate limiting backed by Supabase and upgrades CLI authentication with per-request device secrets. A new migration adds an api_rate_limits table and check_rate_limit RPC function. CLI auth routes (init/poll/verify) now generate and validate poll_secret/verify_secret hashes. The rateLimit function becomes async and RPC-backed. Test infrastructure is extended across 11 test files to mock the service-layer RPC calls.

Changes

Durable Rate Limiting & CLI Device Secrets

Layer / File(s) Summary
Database schema & rate-limit infrastructure
supabase/migrations/20260609200740_security_sweep_cli_auth_rate_limits.sql
Migration adds api_rate_limits table, check_rate_limit SECURITY DEFINER function, updates cli_auth_codes with poll_secret_hash, verify_secret_hash, redeemed_at columns, removes prior RLS policies.
Rate-limit library & unit tests
apps/web/lib/rate-limit.ts, apps/web/__tests__/unit/rate-limit.test.ts
rateLimit becomes async and calls check_rate_limit RPC; returns 429+Retry-After on deny and 503 on RPC error; unit tests cover allow/deny/error cases.
CLI device-secret helpers
apps/web/lib/api/cli-auth.ts
Adds createCliDeviceSecret() and hashCliDeviceSecret(secret) for per-request device secret generation and hashing.
CLI auth route handlers
apps/web/app/api/auth/cli/init/route.ts, apps/web/app/api/auth/cli/poll/route.ts, apps/web/app/api/auth/cli/verify/route.ts
Init generates and returns poll_secret and verify_secret (stores only hashes), verify requires verify_secret and matches verify_secret_hash, poll requires poll_secret and redeems with redeemed_at guard.
Web verify page & CLI command
apps/web/app/cli/verify/page.tsx, packages/cli/src/commands/login.ts, apps/web/e2e/golden-path/cli-verify.spec.ts
Verify page requires verify_secret; CLI loginCommand requires poll_secret from init and includes it in poll requests; E2E helper builds verify URLs including verify_secret.
Async rate-limit adoption at callsites
Multiple API routes (comments, follow, messages, posts, upload, reactions)
Handlers now await rateLimit(...) before checking/early-returning, ensuring correct async control flow for limiter responses.
AI caption validation & rate limiting
apps/web/app/api/ai/generate-caption/route.ts, apps/web/__tests__/api/ai-caption.test.ts
Adds guarded JSON parsing, requires images be first-party post-images URLs, enforces per-user minute/day durable rate limits before Anthropic call; tests added for URL policy and quota-denial behavior.
Usage submission safeguards
apps/web/app/api/usage/submit/route.ts, apps/web/__tests__/api/usage-submit.test.ts
Adds max entry count and body-size limits, readJsonBodyWithLimit helper (413 on oversized body), duplicate-date checks, bounded per-request concurrency for processing, and tests for new validations.
Test infrastructure: service client mocking
apps/web/__tests__/api/*.test.ts, apps/web/__tests__/flows/*.test.ts
Introduces mockServiceClient.rpc and vi.mock('@/lib/supabase/service') across tests; provides mockAllowedRpc() helper to make RPC check_rate_limit deterministic.
Migration safety test
apps/web/__tests__/unit/migration-safety.test.ts
New unit test validates the migration revokes public privileges on cli_auth_codes and removes pending-view policies.
Dependency updates
apps/web/package.json, packages/cli/package.json, package.json
Pins/bumps several runtime and dev deps (e.g., @supabase/supabase-js to 2.108.1, vitest bumps, root overrides expanded).

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly Related PRs

  • ohong/straude#125: Related updates to usage submission implementation and tests affecting apps/web/app/api/usage/submit/route.ts.
  • ohong/straude#51: Related to Supabase migration and bucket/attachment access changes that touch storage and RLS policy assumptions.

"🐰 I tunneled through SQL and tests so neat,
secrets hashed and rate limits keep the beat,
RPCs now guard each API request,
steady as carrots, your server's at rest."

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 11.43% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title '[codex] Harden CLI auth and durable rate limits' directly reflects the primary changes: CLI authentication hardening and the replacement of in-memory rate limiting with durable Supabase-backed rate limits.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/security-sweep

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.

❤️ Share

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

@coderabbitai coderabbitai 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.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
apps/web/app/api/usage/submit/route.ts (1)

466-483: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Validate each entries[] item before reading date and data.

body.entries is still untrusted JSON here, but the loop dereferences entry.date and entry.data immediately. Payloads like {"entries":[null]} or {"entries":[{"date":"2026-06-09"}]} will throw and turn a bad request into a 500 instead of a 400. Add a shape check before calling isValidDate() / validateEntry().

Suggested fix
   const seenDates = new Set<string>();
   for (const entry of body.entries) {
+    if (
+      !entry
+      || typeof entry !== "object"
+      || typeof entry.date !== "string"
+      || !entry.data
+      || typeof entry.data !== "object"
+    ) {
+      return NextResponse.json(
+        { error: "Each entry must include a date and data object" },
+        { status: 400 },
+      );
+    }
+
     if (!isValidDate(entry.date)) {
       return NextResponse.json({ error: `Invalid date: ${entry.date}` }, { status: 400 });
     }
     if (seenDates.has(entry.date)) {
       return NextResponse.json({ error: `Duplicate date: ${entry.date}` }, { status: 400 });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/app/api/usage/submit/route.ts` around lines 466 - 483, The loop over
body.entries dereferences entry.date and entry.data without validating entry's
shape; add a guard at the top of the loop to ensure each entry is a non-null
object with a string date and a defined data field (e.g., typeof entry ===
'object' && entry !== null && typeof entry.date === 'string' && 'data' in entry)
and return a 400 NextResponse.json error when the shape is invalid before
calling isValidDate, isWithinBackfillWindow, or validateEntry; keep using the
existing symbols seenDates, isValidDate, isWithinBackfillWindow, validateEntry
and preserve the same error-response pattern.
apps/web/e2e/golden-path/cli-verify.spec.ts (1)

45-52: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

This test no longer exercises the redirect contract.

Lines 45-52 still look for an anchor, but apps/web/app/cli/verify/page.tsx now renders a button that navigates with router.push. isVisible stays false, so the assertions are skipped and the test passes even if next is broken. Click the button and assert the resulting /login?next=... URL instead.

Suggested fix
-    const signInLink = page.locator('a:has-text("Sign in to authorize")');
-    const isVisible = await signInLink.isVisible().catch(() => false);
+    const signInButton = page.getByRole("button", { name: "Sign in to authorize" });
+    const isVisible = await signInButton.isVisible().catch(() => false);

     if (isVisible) {
-      const href = await signInLink.getAttribute("href");
-      expect(href).toContain("/login");
-      expect(href).toContain("next=");
+      await signInButton.click();
+      await expect(page).toHaveURL(/\/login\?next=/);
+      await expect(page).toHaveURL(/verify_secret=/);
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/e2e/golden-path/cli-verify.spec.ts` around lines 45 - 52, The test
currently checks for an anchor but the component in cli/verify/page.tsx now
renders a button that navigates with router.push, so update the test to click
the button and assert the resulting URL includes the redirect query;
specifically replace the signInLink anchor checks with a locate-and-click of the
button text "Sign in to authorize" (use the same text locator), then wait for
navigation and assert that page.url() contains "/login?next=" to verify the
redirect contract.
🧹 Nitpick comments (1)
apps/web/__tests__/unit/migration-safety.test.ts (1)

186-202: 🏗️ Heavy lift

This only proves the SQL text, not that the migration can run.

These assertions still pass if the migration is unrunnable because of statement ordering or constraint validation against legacy rows. Add one execution-based fixture that seeds an old cli_auth_codes row and applies this migration inside a transaction so deploy-time failures are caught here.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/__tests__/unit/migration-safety.test.ts` around lines 186 - 202, The
current test only inspects SQL text; add an execution-based sub-test inside the
"latest cli_auth_codes hardening removes public grants and pending-code select
policies" test that uses getLatestMigrationMatching to get the migration
content, begins a DB transaction, seeds a legacy public.cli_auth_codes row (with
columns needed to trigger constraint/policy checks), applies the migration SQL
(execute the content against the test DB) inside that transaction, and then
asserts the migration ran without throwing and that the resulting
grants/policies are as expected; ensure you use the same migrations variable and
the migration content from getLatestMigrationMatching and roll back the
transaction after the assertions to keep the fixture isolated.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/web/app/api/ai/generate-caption/route.ts`:
- Around line 26-34: The handler assumes request.json() returns an object and
destructures images and usage directly which throws for non-object JSON (null,
number, string); update the logic around request.json() in route.ts to validate
that the parsed body is a plain object before destructuring: call await
request.json() into body, check typeof body === "object" && body !== null (or
use Array.isArray/body instanceof Object as needed), and if the check fails
return a 400 JSON error; then safely extract images and usage from the validated
body.

In `@apps/web/app/api/auth/cli/init/route.ts`:
- Around line 46-48: The current construction of appUrl uses NEXT_PUBLIC_APP_URL
with a hardcoded production fallback which can cause incorrect verify links;
modify the logic that computes appUrl (the code that sets appUrl and builds
verifyUrl) to use request.nextUrl.origin as the fallback when
NEXT_PUBLIC_APP_URL is unset (or alternatively throw an explicit error if an app
URL must be provided), then rebuild verifyUrl with that origin and the existing
URLSearchParams (code and verify_secret) so verifyUrl points to the actual
request origin rather than "https://straude.com".

In `@apps/web/app/cli/verify/page.tsx`:
- Around line 36-39: The conditional that checks (!code || !verifySecret) in the
page component currently returns a message that only mentions a missing
authorization code; update the user-facing message to a generic "invalid or
incomplete authorization link" (or similar) so it correctly covers both missing
code and missing verifySecret. Locate the check around the variables code and
verifySecret in page.tsx and replace the text inside the <p> element rendered
for that branch to reflect the generic message.

In `@supabase/migrations/20260609200740_security_sweep_cli_auth_rate_limits.sql`:
- Around line 5-30: The migration updates on table cli_auth_codes are ordered
such that status is changed to 'expired' before the cli_auth_codes_status_check
is widened and before new columns poll_secret_hash and verify_secret_hash exist,
which can fail; reorder operations: first ALTER TABLE to ADD COLUMN IF NOT
EXISTS poll_secret_hash, verify_secret_hash, redeemed_at, then DROP and re-ADD
the cli_auth_codes_status_check to include 'expired', then backfill or UPDATE
legacy rows (e.g., set status = 'expired' or populate secret hashes) so no rows
violate the new logic, and only after that DROP/ADD the
cli_auth_codes_active_secrets_check to enforce the stricter secret-nonnull
constraint.
- Around line 87-88: The unconditional table-wide prune using "DELETE FROM
public.api_rate_limits WHERE expires_at < v_now - interval '1 hour';" must be
removed from the request path; instead either move this cleanup to an
out-of-band job (cron/pg_cron/worker) that runs periodically for
public.api_rate_limits, or make the prune opportunistic inside the RPC by
replacing the global DELETE with a bounded/conditional operation (e.g. DELETE
... WHERE expires_at < v_now - interval '1 hour' ORDER BY expires_at LIMIT <N>
or DELETE only for the caller's bucket ids), ensuring you reference the
expires_at/v_now logic and avoid full-table writes from the hot RPC.

---

Outside diff comments:
In `@apps/web/app/api/usage/submit/route.ts`:
- Around line 466-483: The loop over body.entries dereferences entry.date and
entry.data without validating entry's shape; add a guard at the top of the loop
to ensure each entry is a non-null object with a string date and a defined data
field (e.g., typeof entry === 'object' && entry !== null && typeof entry.date
=== 'string' && 'data' in entry) and return a 400 NextResponse.json error when
the shape is invalid before calling isValidDate, isWithinBackfillWindow, or
validateEntry; keep using the existing symbols seenDates, isValidDate,
isWithinBackfillWindow, validateEntry and preserve the same error-response
pattern.

In `@apps/web/e2e/golden-path/cli-verify.spec.ts`:
- Around line 45-52: The test currently checks for an anchor but the component
in cli/verify/page.tsx now renders a button that navigates with router.push, so
update the test to click the button and assert the resulting URL includes the
redirect query; specifically replace the signInLink anchor checks with a
locate-and-click of the button text "Sign in to authorize" (use the same text
locator), then wait for navigation and assert that page.url() contains
"/login?next=" to verify the redirect contract.

---

Nitpick comments:
In `@apps/web/__tests__/unit/migration-safety.test.ts`:
- Around line 186-202: The current test only inspects SQL text; add an
execution-based sub-test inside the "latest cli_auth_codes hardening removes
public grants and pending-code select policies" test that uses
getLatestMigrationMatching to get the migration content, begins a DB
transaction, seeds a legacy public.cli_auth_codes row (with columns needed to
trigger constraint/policy checks), applies the migration SQL (execute the
content against the test DB) inside that transaction, and then asserts the
migration ran without throwing and that the resulting grants/policies are as
expected; ensure you use the same migrations variable and the migration content
from getLatestMigrationMatching and roll back the transaction after the
assertions to keep the fixture isolated.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: e3449a9e-51b5-4e86-a06b-6d556e7d316a

📥 Commits

Reviewing files that changed from the base of the PR and between f88fdf7 and 89143f6.

⛔ Files ignored due to path filters (1)
  • bun.lock is excluded by !**/*.lock
📒 Files selected for processing (34)
  • apps/web/__tests__/api/ai-caption.test.ts
  • apps/web/__tests__/api/auth-cli.test.ts
  • apps/web/__tests__/api/comment-email-notifications.test.ts
  • apps/web/__tests__/api/social.test.ts
  • apps/web/__tests__/api/upload.test.ts
  • apps/web/__tests__/api/usage-submit.test.ts
  • apps/web/__tests__/flows/cli-push-flow.test.ts
  • apps/web/__tests__/flows/post-lifecycle.test.ts
  • apps/web/__tests__/flows/social-interactions.test.ts
  • apps/web/__tests__/flows/web-import-flow.test.ts
  • apps/web/__tests__/unit/migration-safety.test.ts
  • apps/web/__tests__/unit/rate-limit.test.ts
  • apps/web/app/api/ai/generate-caption/route.ts
  • apps/web/app/api/auth/cli/init/route.ts
  • apps/web/app/api/auth/cli/poll/route.ts
  • apps/web/app/api/auth/cli/verify/route.ts
  • apps/web/app/api/comments/[id]/reactions/route.ts
  • apps/web/app/api/comments/[id]/route.ts
  • apps/web/app/api/follow/[username]/route.ts
  • apps/web/app/api/messages/route.ts
  • apps/web/app/api/posts/[id]/comments/route.ts
  • apps/web/app/api/posts/[id]/kudos/route.ts
  • apps/web/app/api/upload/route.ts
  • apps/web/app/api/usage/submit/route.ts
  • apps/web/app/cli/verify/page.tsx
  • apps/web/e2e/golden-path/cli-verify.spec.ts
  • apps/web/lib/api/cli-auth.ts
  • apps/web/lib/rate-limit.ts
  • apps/web/package.json
  • package.json
  • packages/cli/__tests__/commands/login.test.ts
  • packages/cli/package.json
  • packages/cli/src/commands/login.ts
  • supabase/migrations/20260609200740_security_sweep_cli_auth_rate_limits.sql

Comment on lines +26 to 34
let body: { images?: unknown; usage?: CaptionUsage };
try {
body = await request.json();
} catch {
return NextResponse.json({ error: "Invalid JSON" }, { status: 400 });
}

const { images, usage } = body;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Guard non-object JSON payloads before destructuring.

request.json() can return valid non-object JSON (null, number, string). Destructuring body then throws and turns a client error into a 500.

Suggested fix
-  let body: { images?: unknown; usage?: CaptionUsage };
+  let body: unknown;
   try {
     body = await request.json();
   } catch {
     return NextResponse.json({ error: "Invalid JSON" }, { status: 400 });
   }
 
-  const { images, usage } = body;
+  if (!body || typeof body !== "object" || Array.isArray(body)) {
+    return NextResponse.json({ error: "Invalid request body" }, { status: 400 });
+  }
+  const { images, usage } = body as { images?: unknown; usage?: CaptionUsage };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/app/api/ai/generate-caption/route.ts` around lines 26 - 34, The
handler assumes request.json() returns an object and destructures images and
usage directly which throws for non-object JSON (null, number, string); update
the logic around request.json() in route.ts to validate that the parsed body is
a plain object before destructuring: call await request.json() into body, check
typeof body === "object" && body !== null (or use Array.isArray/body instanceof
Object as needed), and if the check fails return a 400 JSON error; then safely
extract images and usage from the validated body.

Comment on lines +46 to +48
const appUrl = (process.env.NEXT_PUBLIC_APP_URL ?? "https://straude.com").replace(/\/+$/, "");
const params = new URLSearchParams({ code, verify_secret: verifySecret });
const verifyUrl = `${appUrl}/cli/verify?${params.toString()}`;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Derive verify_url from the current request origin instead of a production fallback.

Line 46 falls back to https://straude.com when NEXT_PUBLIC_APP_URL is unset. In any staging or self-hosted deployment, packages/cli/src/commands/login.ts will open the browser on the wrong origin and send code plus verify_secret there. Use request.nextUrl.origin as the fallback, or fail fast if the app URL is required.

Suggested fix
-  const appUrl = (process.env.NEXT_PUBLIC_APP_URL ?? "https://straude.com").replace(/\/+$/, "");
+  const appUrl = (process.env.NEXT_PUBLIC_APP_URL ?? request.nextUrl.origin).replace(/\/+$/, "");
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/app/api/auth/cli/init/route.ts` around lines 46 - 48, The current
construction of appUrl uses NEXT_PUBLIC_APP_URL with a hardcoded production
fallback which can cause incorrect verify links; modify the logic that computes
appUrl (the code that sets appUrl and builds verifyUrl) to use
request.nextUrl.origin as the fallback when NEXT_PUBLIC_APP_URL is unset (or
alternatively throw an explicit error if an app URL must be provided), then
rebuild verifyUrl with that origin and the existing URLSearchParams (code and
verify_secret) so verifyUrl points to the actual request origin rather than
"https://straude.com".

Comment thread apps/web/app/cli/verify/page.tsx
Comment on lines +5 to +30
UPDATE public.cli_auth_codes
SET status = 'expired'
WHERE status IN ('pending', 'completed')
AND expires_at > now();

ALTER TABLE public.cli_auth_codes
ADD COLUMN IF NOT EXISTS poll_secret_hash text,
ADD COLUMN IF NOT EXISTS verify_secret_hash text,
ADD COLUMN IF NOT EXISTS redeemed_at timestamptz;

ALTER TABLE public.cli_auth_codes
DROP CONSTRAINT IF EXISTS cli_auth_codes_status_check;

ALTER TABLE public.cli_auth_codes
ADD CONSTRAINT cli_auth_codes_status_check
CHECK (status IN ('pending', 'completed', 'expired', 'used'));

ALTER TABLE public.cli_auth_codes
DROP CONSTRAINT IF EXISTS cli_auth_codes_active_secrets_check;

ALTER TABLE public.cli_auth_codes
ADD CONSTRAINT cli_auth_codes_active_secrets_check
CHECK (
status = 'expired'
OR (poll_secret_hash IS NOT NULL AND verify_secret_hash IS NOT NULL)
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Backfill legacy cli_auth_codes rows before validating the new checks.

This sequence can fail on existing data. status = 'expired' is written before cli_auth_codes_status_check is widened to allow expired, and the later cli_auth_codes_active_secrets_check will still reject any legacy pending / completed / used row that has null secret hashes. Reorder this so the new columns exist first, the status check is widened, all pre-secret rows are marked terminal or backfilled, and only then is the stricter secrets check validated.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@supabase/migrations/20260609200740_security_sweep_cli_auth_rate_limits.sql`
around lines 5 - 30, The migration updates on table cli_auth_codes are ordered
such that status is changed to 'expired' before the cli_auth_codes_status_check
is widened and before new columns poll_secret_hash and verify_secret_hash exist,
which can fail; reorder operations: first ALTER TABLE to ADD COLUMN IF NOT
EXISTS poll_secret_hash, verify_secret_hash, redeemed_at, then DROP and re-ADD
the cli_auth_codes_status_check to include 'expired', then backfill or UPDATE
legacy rows (e.g., set status = 'expired' or populate secret hashes) so no rows
violate the new logic, and only after that DROP/ADD the
cli_auth_codes_active_secrets_check to enforce the stricter secret-nonnull
constraint.

Comment on lines +87 to +88
DELETE FROM public.api_rate_limits
WHERE expires_at < v_now - interval '1 hour';

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Avoid global row pruning on every rate-limit check.

Every RPC call does a table-wide DELETE before touching the caller's bucket. Since this function now fronts multiple hot endpoints, that turns rate limiting into shared cleanup work and will create unnecessary write/VACUUM churn under load. Move pruning out of the request path or make it opportunistic instead of unconditional.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@supabase/migrations/20260609200740_security_sweep_cli_auth_rate_limits.sql`
around lines 87 - 88, The unconditional table-wide prune using "DELETE FROM
public.api_rate_limits WHERE expires_at < v_now - interval '1 hour';" must be
removed from the request path; instead either move this cleanup to an
out-of-band job (cron/pg_cron/worker) that runs periodically for
public.api_rate_limits, or make the prune opportunistic inside the RPC by
replacing the global DELETE with a bounded/conditional operation (e.g. DELETE
... WHERE expires_at < v_now - interval '1 hour' ORDER BY expires_at LIMIT <N>
or DELETE only for the caller's bucket ids), ensuring you reference the
expires_at/v_now logic and avoid full-table writes from the hot RPC.

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