Skip to content

Security/p0 hardening#18

Open
dmvasheka wants to merge 8 commits into
mainfrom
security/p0-hardening
Open

Security/p0 hardening#18
dmvasheka wants to merge 8 commits into
mainfrom
security/p0-hardening

Conversation

@dmvasheka

Copy link
Copy Markdown
Owner

No description provided.

dmitrovasheka and others added 5 commits June 12, 2026 00:12
- .github/workflows/ci.yml: lint/typecheck/test/build via turbo on PR+push to
  main; gitleaks secret scan; pnpm audit (non-blocking for now)
- .dockerignore: .env* and local state no longer enter the Railway image
  build context (COPY . . previously baked local secrets into layers)
- Node engines >=18 -> >=20 (+.nvmrc); fixes apps/docs (Next 16) typecheck
- repair broken scripts: web check-types used Next15-only 'next typegen';
  db/ai lint had no eslint config and eslint v8 vs flat-config v9 conflict
- fix all 24 lint warnings in packages/ai and packages/db (proper types
  instead of any, globalEnv declared in turbo.json)
- web lint ratcheted at 30 pre-existing warnings (new ones fail CI);
  burn-down tracked in IMPROVEMENTS
- turbo: add test task; root 'pnpm test'

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…try init

- GET /api/health: 200 ok/degraded (Redis is non-fatal cache), 503 when DB
  is unreachable; for Railway healthcheck and uptime monitors (TDD, 4 tests)
- Sentry initializes only when SENTRY_DSN is set; tracesSampleRate via env

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Covers cursor validation (incl. injection-style input), LIKE wildcard
escaping, translation overlay with per-field fallback, and DB error paths.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- @repo/ai reports token usage for every chat/embedding call via an
  onAiUsage hook (no signature changes for the 7 existing call sites)
- AiUsageService records endpoint/model/tokens/cost into ai_usage
  (migration 20260612000001; best-effort — never breaks a request)
- AiBudgetGuard returns 429 on /api/chat and /api/movies/:id/explain once
  today's spend reaches OPENAI_DAILY_BUDGET_USD (unset = unlimited)
- embedding_model column on movies/tv_shows + backfill, so a future
  embedding-model switch knows what to re-embed
- chat already defaults to gpt-4o-mini via OPENAI_CHAT_MODEL_SMALL env
- explain endpoint also throttled 10/min (was unlimited)

TDD: 11 new tests (pricing, recording, fail-open paths, budget guard)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…s race

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@vercel

vercel Bot commented Jun 12, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
m-recommendr-web Ready Ready Preview, Comment Jun 12, 2026 10:05pm
m-recommendr-web-ip4u Ready Ready Preview, Comment Jun 12, 2026 10:05pm

@coderabbitai

coderabbitai Bot commented Jun 12, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@dmvasheka, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 17 minutes and 12 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more credits in the billing tab to continue.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 4ab7d133-34d9-4902-b292-9ac763666f04

📥 Commits

Reviewing files that changed from the base of the PR and between 32a2a11 and 03d4cae.

📒 Files selected for processing (1)
  • turbo.json
📝 Walkthrough

Walkthrough

Adds CI and Node 20 pinning, .dockerignore, Turborepo/globalEnv, health endpoint and Redis/DB checks, conditional Sentry init, AI usage eventing and ai_usage persistence, daily budget guard enforcing OPENAI_DAILY_BUDGET_USD (429), DB/type migrations, tests, and docs updates.

Changes

Security Hardening P1 Implementation

Layer / File(s) Summary
CI/CD and Infrastructure Foundation
.github/workflows/ci.yml, .nvmrc, .dockerignore, turbo.json, package.json, apps/api/package.json, apps/web/package.json
Adds GitHub Actions CI with split lint/test/build steps, pins Node 20, adds .dockerignore, updates Turborepo tasks and globalEnv, and adjusts scripts and pnpm/node setup.
Tooling and lint updates
packages/eslint-config/base.js, packages/ai/eslint.config.mjs, packages/db/eslint.config.mjs, packages/ai/package.json, packages/db/package.json, packages/ai/src/openai.client.ts, packages/db/src/supabase.client.ts
Updates shared ESLint rules, adds flat-config entrypoints for packages, bumps ESLint versions, and adds ESLint suppressions for conditional dotenv requires.
Database Schema & Types
supabase/migrations/20260612000001_add_ai_usage.sql, supabase/migrations/20260612000002_add_check_constraints.sql, supabase/migrations/20260130000001_add_imdb_fields.sql, packages/db/src/generated-types.ts, packages/db/src/types.ts
Adds ai_usage table, embedding_model and IMDb columns with backfill, check constraints, and updates generated and manual DB TypeScript types and function signatures.
AI usage event hooks & callsites
packages/ai/src/usage.ts, packages/ai/src/index.ts, packages/ai/src/chat.ts, packages/ai/src/embeddings.ts
Adds onAiUsage/reportAiUsage contract and emits usage events from chat, explain, and embeddings codepaths; re-exports API and types.
AiUsageService & Module Wiring
apps/api/src/ai-usage/ai-usage.service.ts, apps/api/src/ai-usage/ai-usage.module.ts, apps/api/src/ai-usage/__tests__/*
Implements AiUsageService to compute per-call cost, record usage to ai_usage (best-effort), sum today’s spend, and wires an onAiUsage subscriber on module init.
AI Budget Guard and Integration
apps/api/src/ai-usage/ai-budget.guard.ts, apps/api/src/ai-usage/__tests__/ai-budget.guard.spec.ts
Implements AiBudgetGuard enforcing OPENAI_DAILY_BUDGET_USD (fail-open when unset/invalid, throws 429 when exceeded) with unit tests.
Health Check Subsystem & Sentry
apps/api/src/health/health.service.ts, apps/api/src/health/health.controller.ts, apps/api/src/health/health.module.ts, apps/api/src/instrument.ts, apps/api/src/main.ts, .env.example
Adds GET /api/health aggregating DB and Redis status (ok/degraded/down, 503 on DB down), tests, and conditional Sentry initialization using SENTRY_DSN and SENTRY_TRACES_SAMPLE_RATE.
Endpoint Protection and AppModule Wiring
apps/api/src/chat/chat.controller.ts, apps/api/src/movies/movies.controller.ts, apps/api/src/app.module.ts
Applies AiBudgetGuard to chat send and movies explain endpoints, tightens throttle for explainMovie, and imports HealthModule and AiUsageModule into AppModule.
Testing, Autocomplete & Utilities
apps/api/src/movies/__tests__/movies.service.autocomplete.spec.ts, apps/api/src/utils/__tests__/cursor.utils.spec.ts, apps/api/src/health/__tests__/health.service.spec.ts, apps/api/src/ai-usage/__tests__/*
Adds unit tests for movies.autocomplete, cursor parsing, health checks, ai-usage service and guard, and related test mocks.
Project Documentation
CLAUDE.md, IMPROVEMENTS.md, SESSION_RESUME.md
Updates project notes and session resumes to document CI, health endpoint, Sentry, ai_usage migration/backfill, OPENAI_DAILY_BUDGET_USD, and increased test counts.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Poem

🐰 In tidy builds and guarded queues I bound,
Sentry hums while health checks look around.
Usage counts hop in rows and then are stored,
Budgets stand watch — the guard keeps costs on board.
A rabbit cheers: landed, tested, well secured.

🚥 Pre-merge checks | ✅ 2 | ❌ 3

❌ Failed checks (2 warnings, 1 inconclusive)

Check name Status Explanation Resolution
Description check ⚠️ Warning No pull request description was provided by the author, making it impossible to assess relevance to the changeset. Add a description explaining the purpose and scope of the changes, including CI/CD improvements, health checks, AI usage tracking, and database schema updates.
Docstring Coverage ⚠️ Warning Docstring coverage is 54.55% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'Security/p0 hardening' is vague and uses generic terms that don't clearly convey the specific changes in the changeset, which includes CI/CD setup, health checks, AI budget controls, and database migrations. Consider a more specific title that summarizes the main changes, such as 'Add CI/CD, health endpoint, AI budget controls, and database migrations' or focus on the primary feature.
✅ Passed checks (2 passed)
Check name Status Explanation
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 unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch security/p0-hardening

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: 11

Caution

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

⚠️ Outside diff range comments (1)
SESSION_RESUME.md (1)

35-40: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

P0 closure blockers are identified but key rotation timeline is critical.

Lines 35–40 list actions without which P0 is not closed:

  1. Key rotation (overdue since 2026-01-16) — BLOCKER
  2. Railway ADMIN_API_KEY setup
  3. Coordinated API+Web deployment
  4. Create PR

The key rotation is overdue for ~5 months. This is a critical security issue. IMPROVEMENTS.md §17.1 (line 2048) documents the overdue task, and SESSION_RESUME.md correctly calls it out.

Recommendation: Escalate key rotation to a hard requirement before merging PR. Current exposed keys (per IMPROVEMENTS.md §8.7, line 1089: "4 critical, 2 high-risk") should be rotated immediately upon PR approval.

Clarification: Line 39 notes "Локально уже добавлен в .env этой машины" — implies machine-specific key. Confirm with team whether prod ADMIN_API_KEY should be per-environment (dev/staging/prod) or same across all (not recommended due to key compromise risk). Document the decision.

🤖 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 `@SESSION_RESUME.md` around lines 35 - 40, SESSION_RESUME.md calls out an
overdue key rotation for ADMIN_API_KEY and other exposed keys; before merging,
rotate the compromised keys immediately and escalate rotation to a hard
requirement: generate new ADMIN_API_KEY (openssl rand -hex 32), update Railway
variable ADMIN_API_KEY and each environment's secret (.env entries
per-environment rather than reusing one key), revoke old keys, deploy API+Web
together, and update the PR security/p0-hardening description plus
IMPROVEMENTS.md §17.1 and §8.7 to record rotation status and the decision about
per-environment keys and machine-specific .env handling.
🧹 Nitpick comments (3)
IMPROVEMENTS.md (1)

2091-2093: 💤 Low value

Clarify lint-warning fix status and test for regressions.

Line 2091 claims "Исправлены все 24 lint-warnings в packages/ai и packages/db", but the note implies these were blocking CI. If all 24 are fixed, the CI should pass cleanly. Line 2093 notes "⚠️ web lint — храповик --max-warnings 30" — implying web lint still has warnings (30 threshold).

For clarity in docs:

  • Explicitly state: "packages/ai and packages/db: 0 warnings (fixed)" vs. "apps/web: 30 warnings (throttle threshold set)".
  • Ensure CI actually fails on any new lint warnings in api/ai/db packages.
🤖 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 `@IMPROVEMENTS.md` around lines 2091 - 2093, Update the IMPROVEMENTS.md text to
explicitly state packages/ai and packages/db have 0 lint warnings (e.g.,
"packages/ai: 0 warnings (fixed); packages/db: 0 warnings (fixed)") and clarify
apps/web retains 30 warnings with the throttle note ("apps/web: 30 warnings
(throttle threshold set)"). Then modify the CI lint job (the workflow that runs
lint for api/ai/db packages) to fail on any new warnings by setting the linter
invocation for api/ai/db to use zero-tolerance (e.g., eslint/tsc with
--max-warnings=0 or equivalent) so new lint warnings will break the build;
ensure job names referencing api/ai/db reflect this change.
apps/api/src/ai-usage/__tests__/ai-budget.guard.spec.ts (1)

15-53: ⚡ Quick win

Add a regression test for “service throws → fail-open”.

Please add a case where getTodaySpendUsd() rejects/throws and assert canActivate() still resolves true. That locks in the intended availability behavior.

🤖 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/api/src/ai-usage/__tests__/ai-budget.guard.spec.ts` around lines 15 -
53, Add a test to assert AiBudgetGuard.fail-open behavior when the usage service
throws: create a guard using a usage service whose getTodaySpendUsd() returns a
rejected promise (or throws), call guard.canActivate(ctx()) and assert it
resolves to true; reference AiBudgetGuard, usageServiceWithSpend (or a similar
mocked service), getTodaySpendUsd, and canActivate so the new test mirrors the
existing tests for budget absent/invalid but uses a throwing/rejecting
getTodaySpendUsd to validate availability is preserved.
apps/api/src/movies/__tests__/movies.service.autocomplete.spec.ts (1)

98-100: ⚡ Quick win

Strengthen the error-path assertion to prevent false positives.

rejects.toBeTruthy() can pass for unrelated rejection shapes. Assert the expected error message/type so this test protects the real contract.

Proposed change
-        await expect(service.autocomplete('test')).rejects.toBeTruthy();
+        await expect(service.autocomplete('test')).rejects.toThrow('db down');
🤖 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/api/src/movies/__tests__/movies.service.autocomplete.spec.ts` around
lines 98 - 100, The test currently uses a loose assertion rejects.toBeTruthy()
which can mask unrelated errors; update the expectation to assert the exact
error shape/message returned when Supabase errors: after setting
queryState.result = { data: null, error: { message: 'db down' } }, change the
assertion to expect service.autocomplete('test') to reject with an Error (or the
specific error class) containing 'db down' (e.g., rejects.toThrow('db down') or
rejects.toMatchObject({ message: 'db down' })), so the test for autocomplete on
Service.autocomplete validates the specific error contract.
🤖 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 @.github/workflows/ci.yml:
- Around line 24-25: The checkout steps currently use actions/checkout@v4
without disabling credential persistence; update both checkout steps (the
actions/checkout@v4 uses at the top-level and the second checkout in the other
job) to include persist-credentials: false so the runner does not write the
token to local git config—locate the checkout steps referencing
actions/checkout@v4 and add persist-credentials: false to each step.
- Around line 24-30: The workflow uses mutable action tags (e.g.,
actions/checkout@v4, pnpm/action-setup@v4, actions/setup-node@v4) which can be
altered by upstream tag updates; replace each mutable tag with the corresponding
immutable commit SHA for that action (fetch the current commit SHA from the
action's GitHub repository and substitute it in the uses: field), and do the
same for the other occurrences noted in the comment (lines referencing `@v`* such
as the entries around lines 50–55, 59–63) so all actions are pinned to exact
SHAs (keep the same action owner/name but use the full commit SHA).
- Around line 1-9: The workflow is missing an explicit least-privilege
permissions block; add a top-level permissions mapping under the "name: CI"
workflow (and if necessary override per job) that only grants the specific
scopes the workflow actually needs (for example: permissions: contents: read,
packages: read) instead of relying on defaults, and for any job that requires
additional rights adjust that job's permissions block inside "jobs" to narrowly
grant only what it needs.

In `@apps/api/src/ai-usage/__tests__/ai-usage.service.spec.ts`:
- Around line 16-18: The Supabase mock in the test discards the arguments to
select() and gte(), so update the mock to capture and assert those args for
getTodaySpendUsd(): replace the current select: () => ({ gte: () =>
Promise.resolve(state.selectResult) }) with a jest.fn-based mock that records
the select(columns) argument and returns an object whose gte(column, value) is
also a jest.fn that asserts column === 'created_at' (or the expected column) and
that value equals the start-of-day date your code uses (e.g., new
Date(...).toISOString() or state.startOfToday), then returns
Promise.resolve(state.selectResult); ensure you reference the getTodaySpendUsd()
call in the test and assert the mock functions were called with the expected
parameters.

In `@apps/api/src/ai-usage/ai-budget.guard.ts`:
- Around line 22-30: The guard currently fails closed if
AiUsageService.getTodaySpendUsd() throws; wrap the spend lookup and comparison
in a try/catch inside the method containing the existing logic (where spend is
assigned and compared to budget), call this.logger.warn or this.logger.error
with the caught error for observability, and on any exception return true to
preserve the fail-open (availability-first) behavior instead of throwing
HttpException(HttpStatus.TOO_MANY_REQUESTS); keep the existing warn+throw path
when spend is successfully retrieved and exceeds budget.

In `@apps/api/src/ai-usage/ai-usage.service.ts`:
- Around line 63-72: getTodaySpendUsd currently selects all cost_usd rows and
reduces in Node; change it to perform a DB-side aggregation (SUM) to avoid
fetching every row: in the getTodaySpendUsd method/query against the ai_usage
table, replace the .select('cost_usd').gte('created_at',
startOfDay.toISOString()) call with a single aggregate select like
.select('sum(cost_usd) as total') (or DB client equivalent), filter by
created_at the same way, then read the returned total value (handle
null/undefined by returning 0) and preserve existing error logging
(this.logger.warn on error) and numeric return type.

In `@IMPROVEMENTS.md`:
- Line 2126: Update the note about migration files into an explicit actionable
item: either convert the buried clarification into a checklist TODO like "[ ]
Consolidate migrations: move the single file in packages/db/supabase/migrations
into the canonical supabase/migrations/ directory" or mark it completed "[x]" if
you already consolidated; then remove the duplicate in
packages/db/supabase/migrations so only supabase/migrations/*.sql remain as the
canonical location per guidelines and ensure the IMPROVEMENTS.md line
referencing "packages/db/supabase/migrations" is updated to reflect the final
state.

In `@packages/ai/src/usage.ts`:
- Around line 6-10: AiUsageEvent currently only allows 'chat'|'embedding',
causing generateMovieExplanation() to report as 'chat' and hide explain requests
in ai_usage.endpoint; update the AiUsageEvent type to include a distinct kind
(e.g., 'movie_explanation' or 'explain_movie'), change
generateMovieExplanation() to emit that new kind, and ensure any code that
persists event.kind into ai_usage.endpoint (the API module that writes ai_usage)
continues to accept and store the new value so explain requests are tracked
separately from chat.

In `@packages/db/src/supabase.client.ts`:
- Line 34: The connectivity probe uses supabase.from('movies').select('count'),
which requests a non-existent column and can yield false negatives; change the
probe to request existing data instead (e.g. use
supabase.from('movies').select('*') or select('id', { count: 'exact' }) and add
a lightweight limit(1)) so the query verifies connectivity without relying on a
non-existent "count" column; update the call to replace
supabase.from('movies').select('count') with one of these alternatives in the
connectivity probe function.

In `@SESSION_RESUME.md`:
- Line 3: Update the ambiguous branch description and clarify environment
variable setup: change the phrase "включает и P1" to "включает также реализацию
P1 (инженерная база)" in the Branch line, and add explicit instructions for
ADMIN_API_KEY and deployment coordination — state that local developers should
generate their own ADMIN_API_KEY (e.g., via openssl) and store it in .env (which
remains out of git), while Production/Staging must set the same ADMIN_API_KEY
for both API and Web via Railway Variables UI or a secrets manager; also add
deployment guidance to coordinate API and Web releases (or provide a
compatibility/fallback plan so new API returning 401 doesn’t break an old Web)
to avoid mismatched inter-service auth.

In `@supabase/migrations/20260612000001_add_ai_usage.sql`:
- Around line 7-9: Add DB-level CHECK constraints to prevent negative values on
the usage/accounting columns: add CHECK (prompt_tokens >= 0), CHECK
(completion_tokens >= 0), and CHECK (cost_usd >= 0). Implement these in the
migration that defines or alters the table containing prompt_tokens,
completion_tokens, and cost_usd (use explicit constraint names like
chk_prompt_tokens_nonneg, chk_completion_tokens_nonneg, chk_cost_usd_nonneg) so
that the schema enforces non-negative invariants in addition to the existing NOT
NULL and DEFAULT clauses.

---

Outside diff comments:
In `@SESSION_RESUME.md`:
- Around line 35-40: SESSION_RESUME.md calls out an overdue key rotation for
ADMIN_API_KEY and other exposed keys; before merging, rotate the compromised
keys immediately and escalate rotation to a hard requirement: generate new
ADMIN_API_KEY (openssl rand -hex 32), update Railway variable ADMIN_API_KEY and
each environment's secret (.env entries per-environment rather than reusing one
key), revoke old keys, deploy API+Web together, and update the PR
security/p0-hardening description plus IMPROVEMENTS.md §17.1 and §8.7 to record
rotation status and the decision about per-environment keys and machine-specific
.env handling.

---

Nitpick comments:
In `@apps/api/src/ai-usage/__tests__/ai-budget.guard.spec.ts`:
- Around line 15-53: Add a test to assert AiBudgetGuard.fail-open behavior when
the usage service throws: create a guard using a usage service whose
getTodaySpendUsd() returns a rejected promise (or throws), call
guard.canActivate(ctx()) and assert it resolves to true; reference
AiBudgetGuard, usageServiceWithSpend (or a similar mocked service),
getTodaySpendUsd, and canActivate so the new test mirrors the existing tests for
budget absent/invalid but uses a throwing/rejecting getTodaySpendUsd to validate
availability is preserved.

In `@apps/api/src/movies/__tests__/movies.service.autocomplete.spec.ts`:
- Around line 98-100: The test currently uses a loose assertion
rejects.toBeTruthy() which can mask unrelated errors; update the expectation to
assert the exact error shape/message returned when Supabase errors: after
setting queryState.result = { data: null, error: { message: 'db down' } },
change the assertion to expect service.autocomplete('test') to reject with an
Error (or the specific error class) containing 'db down' (e.g.,
rejects.toThrow('db down') or rejects.toMatchObject({ message: 'db down' })), so
the test for autocomplete on Service.autocomplete validates the specific error
contract.

In `@IMPROVEMENTS.md`:
- Around line 2091-2093: Update the IMPROVEMENTS.md text to explicitly state
packages/ai and packages/db have 0 lint warnings (e.g., "packages/ai: 0 warnings
(fixed); packages/db: 0 warnings (fixed)") and clarify apps/web retains 30
warnings with the throttle note ("apps/web: 30 warnings (throttle threshold
set)"). Then modify the CI lint job (the workflow that runs lint for api/ai/db
packages) to fail on any new warnings by setting the linter invocation for
api/ai/db to use zero-tolerance (e.g., eslint/tsc with --max-warnings=0 or
equivalent) so new lint warnings will break the build; ensure job names
referencing api/ai/db reflect this change.
🪄 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: dee62b59-a982-4305-abcb-cef229a4278b

📥 Commits

Reviewing files that changed from the base of the PR and between 5151dea and f1e9f76.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (39)
  • .dockerignore
  • .env.example
  • .github/workflows/ci.yml
  • .nvmrc
  • CLAUDE.md
  • IMPROVEMENTS.md
  • SESSION_RESUME.md
  • apps/api/package.json
  • apps/api/src/ai-usage/__tests__/ai-budget.guard.spec.ts
  • apps/api/src/ai-usage/__tests__/ai-usage.service.spec.ts
  • apps/api/src/ai-usage/ai-budget.guard.ts
  • apps/api/src/ai-usage/ai-usage.module.ts
  • apps/api/src/ai-usage/ai-usage.service.ts
  • apps/api/src/app.module.ts
  • apps/api/src/chat/chat.controller.ts
  • apps/api/src/health/__tests__/health.service.spec.ts
  • apps/api/src/health/health.controller.ts
  • apps/api/src/health/health.module.ts
  • apps/api/src/health/health.service.ts
  • apps/api/src/instrument.ts
  • apps/api/src/main.ts
  • apps/api/src/movies/__tests__/movies.service.autocomplete.spec.ts
  • apps/api/src/movies/movies.controller.ts
  • apps/api/src/utils/__tests__/cursor.utils.spec.ts
  • apps/web/package.json
  • package.json
  • packages/ai/eslint.config.mjs
  • packages/ai/package.json
  • packages/ai/src/chat.ts
  • packages/ai/src/embeddings.ts
  • packages/ai/src/index.ts
  • packages/ai/src/openai.client.ts
  • packages/ai/src/usage.ts
  • packages/db/eslint.config.mjs
  • packages/db/package.json
  • packages/db/src/supabase.client.ts
  • packages/eslint-config/base.js
  • supabase/migrations/20260612000001_add_ai_usage.sql
  • turbo.json

Comment thread .github/workflows/ci.yml
Comment thread .github/workflows/ci.yml Outdated
Comment thread .github/workflows/ci.yml Outdated
Comment thread apps/api/src/ai-usage/__tests__/ai-usage.service.spec.ts Outdated
Comment on lines +22 to +30
const spend = await this.aiUsageService.getTodaySpendUsd();
if (spend >= budget) {
this.logger.warn(`Daily AI budget exhausted: $${spend.toFixed(4)} >= $${budget}`);
throw new HttpException(
'Daily AI budget exhausted, try again tomorrow',
HttpStatus.TOO_MANY_REQUESTS,
);
}
return true;

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

Preserve fail-open behavior if spend lookup throws.

The guard currently fails closed on unexpected AiUsageService exceptions. Wrap the spend lookup/comparison in try/catch and allow the request on error to match the documented availability-first contract.

Suggested patch
-        const spend = await this.aiUsageService.getTodaySpendUsd();
-        if (spend >= budget) {
-            this.logger.warn(`Daily AI budget exhausted: $${spend.toFixed(4)} >= $${budget}`);
-            throw new HttpException(
-                'Daily AI budget exhausted, try again tomorrow',
-                HttpStatus.TOO_MANY_REQUESTS,
-            );
-        }
-        return true;
+        try {
+            const spend = await this.aiUsageService.getTodaySpendUsd();
+            if (spend >= budget) {
+                this.logger.warn(`Daily AI budget exhausted: $${spend.toFixed(4)} >= $${budget}`);
+                throw new HttpException(
+                    'Daily AI budget exhausted, try again tomorrow',
+                    HttpStatus.TOO_MANY_REQUESTS,
+                );
+            }
+            return true;
+        } catch (error) {
+            const message = error instanceof Error ? error.message : String(error);
+            this.logger.warn(`Budget check failed open: ${message}`);
+            return true;
+        }
🤖 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/api/src/ai-usage/ai-budget.guard.ts` around lines 22 - 30, The guard
currently fails closed if AiUsageService.getTodaySpendUsd() throws; wrap the
spend lookup and comparison in a try/catch inside the method containing the
existing logic (where spend is assigned and compared to budget), call
this.logger.warn or this.logger.error with the caught error for observability,
and on any exception return true to preserve the fail-open (availability-first)
behavior instead of throwing HttpException(HttpStatus.TOO_MANY_REQUESTS); keep
the existing warn+throw path when spend is successfully retrieved and exceeds
budget.

Comment thread IMPROVEMENTS.md Outdated
Comment thread packages/ai/src/usage.ts
Comment thread packages/db/src/supabase.client.ts Outdated
Comment thread SESSION_RESUME.md
Comment on lines +7 to +9
prompt_tokens INTEGER NOT NULL DEFAULT 0,
completion_tokens INTEGER NOT NULL DEFAULT 0,
cost_usd NUMERIC(12, 8) NOT NULL DEFAULT 0,

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

Add DB-level non-negative constraints for usage accounting fields.

Line 7–9 store financial/accounting data but currently allow negative values. A bad insert can undercount daily spend and weaken budget enforcement. Enforce invariants in schema, not only application code.

Suggested migration hardening
 CREATE TABLE IF NOT EXISTS ai_usage (
     id BIGSERIAL PRIMARY KEY,
     endpoint TEXT NOT NULL,                 -- 'chat' | 'embedding' | ...
     model TEXT NOT NULL,
-    prompt_tokens INTEGER NOT NULL DEFAULT 0,
-    completion_tokens INTEGER NOT NULL DEFAULT 0,
-    cost_usd NUMERIC(12, 8) NOT NULL DEFAULT 0,
+    prompt_tokens INTEGER NOT NULL DEFAULT 0 CHECK (prompt_tokens >= 0),
+    completion_tokens INTEGER NOT NULL DEFAULT 0 CHECK (completion_tokens >= 0),
+    cost_usd NUMERIC(12, 8) NOT NULL DEFAULT 0 CHECK (cost_usd >= 0),
     user_id UUID,                           -- optional attribution
     created_at TIMESTAMPTZ NOT NULL DEFAULT now()
 );
🤖 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/20260612000001_add_ai_usage.sql` around lines 7 - 9, Add
DB-level CHECK constraints to prevent negative values on the usage/accounting
columns: add CHECK (prompt_tokens >= 0), CHECK (completion_tokens >= 0), and
CHECK (cost_usd >= 0). Implement these in the migration that defines or alters
the table containing prompt_tokens, completion_tokens, and cost_usd (use
explicit constraint names like chk_prompt_tokens_nonneg,
chk_completion_tokens_nonneg, chk_cost_usd_nonneg) so that the schema enforces
non-negative invariants in addition to the existing NOT NULL and DEFAULT
clauses.

…h ways

Applied via Supabase Management API (project had been PAUSED by free tier —
production was down; restored first):
- 20260501152500_add_search_vectors (smart-search Phase 1 DB layer)
- 20260505000001_restore_match_translations
- 20260612000001_add_ai_usage (embedding_model backfill: 7697 movies, 3030 tv)

Drift fixes:
- 20260130000001_add_imdb_fields recovered from prod history into the repo
  (was applied via dashboard, never committed)
- migration history table synced (17 entries)
- generated-types.ts regenerated; ai_usage added to manual types.ts
  (manual/generated consolidation tracked in IMPROVEMENTS §18.5)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@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: 2

🤖 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 `@supabase/migrations/20260130000001_add_imdb_fields.sql`:
- Around line 6-11: Add DB-level CHECK constraints to enforce imdb_rating is
between 0.0 and 10.0 and imdb_votes is non-negative for both tables: create
named constraints on movies (e.g., chk_movies_imdb_rating_range,
chk_movies_imdb_votes_nonneg) and on tv_shows (e.g.,
chk_tv_shows_imdb_rating_range, chk_tv_shows_imdb_votes_nonneg) using ALTER
TABLE ... ADD CONSTRAINT ... CHECK (imdb_rating >= 0.0 AND imdb_rating <= 10.0)
and CHECK (imdb_votes >= 0); ensure the migration handles existing constraints
safely (check for constraint existence or use a conditional DO block) so
movies.imdb_rating, movies.imdb_votes, tv_shows.imdb_rating and
tv_shows.imdb_votes are validated at the DB level.
- Line 21: The migration contains a SQL statement "COMMENT ON COLUMN
tv_shows.imdb_votes IS 'Number of IMDb votes'" missing a trailing semicolon
which breaks parsing; update the COMMENT ON COLUMN statement for table tv_shows
and column imdb_votes (the line with COMMENT ON COLUMN tv_shows.imdb_votes IS
'Number of IMDb votes') to include a terminating semicolon so the migration file
parses and applies correctly.
🪄 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: e9abbe9d-2237-480e-af66-a3b2a3c1bce7

📥 Commits

Reviewing files that changed from the base of the PR and between f1e9f76 and ef8178f.

📒 Files selected for processing (6)
  • IMPROVEMENTS.md
  • SESSION_RESUME.md
  • apps/api/src/ai-usage/ai-usage.service.ts
  • packages/db/src/generated-types.ts
  • packages/db/src/types.ts
  • supabase/migrations/20260130000001_add_imdb_fields.sql
✅ Files skipped from review due to trivial changes (4)
  • packages/db/src/types.ts
  • SESSION_RESUME.md
  • IMPROVEMENTS.md
  • packages/db/src/generated-types.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/api/src/ai-usage/ai-usage.service.ts

Comment on lines +6 to +11
ALTER TABLE movies ADD COLUMN IF NOT EXISTS imdb_rating DECIMAL(3,1);
ALTER TABLE movies ADD COLUMN IF NOT EXISTS imdb_votes INTEGER;
-- Add IMDb fields to tv_shows table
ALTER TABLE tv_shows ADD COLUMN IF NOT EXISTS imdb_id TEXT;
ALTER TABLE tv_shows ADD COLUMN IF NOT EXISTS imdb_rating DECIMAL(3,1);
ALTER TABLE tv_shows ADD COLUMN IF NOT EXISTS imdb_votes INTEGER;

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

Add DB-level constraints for IMDb value ranges.

The comments (Lines 17 and 20) define imdb_rating as 0.0-10.0, and imdb_votes should never be negative, but no constraints enforce this. Invalid values can be persisted.

Suggested fix
 ALTER TABLE tv_shows ADD COLUMN IF NOT EXISTS imdb_votes INTEGER;
+
+ALTER TABLE movies
+  ADD CONSTRAINT movies_imdb_rating_range_chk
+  CHECK (imdb_rating IS NULL OR (imdb_rating >= 0.0 AND imdb_rating <= 10.0)) NOT VALID;
+ALTER TABLE movies
+  ADD CONSTRAINT movies_imdb_votes_nonnegative_chk
+  CHECK (imdb_votes IS NULL OR imdb_votes >= 0) NOT VALID;
+
+ALTER TABLE tv_shows
+  ADD CONSTRAINT tv_shows_imdb_rating_range_chk
+  CHECK (imdb_rating IS NULL OR (imdb_rating >= 0.0 AND imdb_rating <= 10.0)) NOT VALID;
+ALTER TABLE tv_shows
+  ADD CONSTRAINT tv_shows_imdb_votes_nonnegative_chk
+  CHECK (imdb_votes IS NULL OR imdb_votes >= 0) NOT VALID;

Also applies to: 17-21

🤖 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/20260130000001_add_imdb_fields.sql` around lines 6 - 11,
Add DB-level CHECK constraints to enforce imdb_rating is between 0.0 and 10.0
and imdb_votes is non-negative for both tables: create named constraints on
movies (e.g., chk_movies_imdb_rating_range, chk_movies_imdb_votes_nonneg) and on
tv_shows (e.g., chk_tv_shows_imdb_rating_range, chk_tv_shows_imdb_votes_nonneg)
using ALTER TABLE ... ADD CONSTRAINT ... CHECK (imdb_rating >= 0.0 AND
imdb_rating <= 10.0) and CHECK (imdb_votes >= 0); ensure the migration handles
existing constraints safely (check for constraint existence or use a conditional
DO block) so movies.imdb_rating, movies.imdb_votes, tv_shows.imdb_rating and
tv_shows.imdb_votes are validated at the DB level.

Comment thread supabase/migrations/20260130000001_add_imdb_fields.sql Outdated
CI was red: pnpm/action-setup 'version: 9' conflicts with packageManager in
package.json — version key removed, packageManager is the single source.

Review fixes:
- workflow: top-level 'permissions: contents: read', persist-credentials:
  false on both checkouts, all actions pinned to commit SHAs (annotated
  tags dereferenced), gitleaks PR comments disabled to match permissions
- @repo/ai: distinct 'explain' usage kind so explanations are tracked
  separately from chat in ai_usage.endpoint
- testConnection probe: select('count') referenced a non-existent column
  (every health check would report DB down) -> select('id').limit(1)
- ai-usage spec asserts select/gte arguments (created_at, UTC midnight)
- migrations: trailing semicolon in recovered add_imdb_fields; duplicate
  packages/db/supabase/migrations/003 removed (identical to 20260117000001);
  new 20260612000002_add_check_constraints (non-negative tokens/cost,
  imdb_rating 0..10, imdb_votes >= 0) — applied to prod, history synced
- docs: ADMIN_API_KEY setup and API+Web deploy coordination in SESSION_RESUME

Skipped finding #5 (budget guard fail-closed on exception): incorrect —
getTodaySpendUsd() never throws by design (internal catch returns 0,
covered by the 'returns 0 when the query fails' test).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
jest resolves workspace packages through their dist entrypoints; on a clean
CI runner the test task ran before any build, so module resolution failed.
test now dependsOn ^build (reproduced locally by removing packages/*/dist).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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.

2 participants