feat: add modular content creation primitive endpoints#390
feat: add modular content creation primitive endpoints#390recoupableorg wants to merge 32 commits intomainfrom
Conversation
New endpoints under POST /api/content/create/: - /image — triggers create-image task - /video — triggers create-video task - /audio — triggers create-audio task - /render — triggers create-render task - /upscale — triggers create-upscale task - /text — inline LLM text generation (no task) DRY shared factories eliminate boilerplate: - triggerPrimitive: one function replaces 5 trigger files - validatePrimitiveBody: shared auth + Zod parsing - createPrimitiveHandler: factory for async handlers - createPrimitiveRoute: shared CORS + dynamic config Existing POST /api/content/create (V1 full pipeline) is untouched. Made-with: Cursor
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds multiple new content-creation API routes and their primitive handlers, new Zod schemas and request validation helpers, a Trigger.dev wrapper, and many JSDoc enhancements across the codebase. Routes include CORS OPTIONS handlers and route-level caching/runtime exports. Changes
Sequence DiagramsequenceDiagram
participant Client as Client
participant Route as Route Module
participant Validator as validatePrimitiveBody
participant Auth as validateAuthContext
participant Handler as Primitive Handler
participant ExtAPI as External API / FAL / TwelveLabs
participant Trigger as Trigger.dev
participant Response as NextResponse
Client->>Route: POST /api/content/create/{type} + body
Route->>Validator: validatePrimitiveBody(request, schema)
Validator->>Validator: parse JSON & Zod validate
alt validation fails
Validator->>Response: 400 JSON + CORS
else
Validator->>Auth: validateAuthContext(request)
alt auth fails
Auth->>Response: auth error (NextResponse)
else
Auth->>Handler: call handler with accountId + payload
alt handler uses external API
Handler->>ExtAPI: fetch / fal.subscribe / TwelveLabs
alt external error
ExtAPI-->>Handler: non-OK / error
Handler->>Response: 502/500 JSON + CORS
else
ExtAPI-->>Handler: result data
Handler->>Response: 200 JSON + CORS (normalized)
end
else handler triggers background task
Handler->>Trigger: tasks.trigger("create-render", payload)
Trigger-->>Handler: handle
Handler->>Response: 202 JSON { runId, status } + CORS
end
end
end
Response-->>Client: HTTP response
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ❌ 1❌ Failed checks (1 warning)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 11
🧹 Nitpick comments (2)
lib/content/primitives/createTextHandler.ts (1)
11-80: SplitcreateTextHandlerinto smaller helpers.Line 11–80 is doing validation, prompt composition, upstream transport, response normalization, and HTTP mapping in one function. Breaking this up will improve maintainability and testability.
As per coding guidelines: "Keep functions under 50 lines" and "Single responsibility per function".
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/content/primitives/createTextHandler.ts` around lines 11 - 80, Split the large createTextHandler into smaller helpers: extract validation into keep using validatePrimitiveBody with createTextBodySchema, move prompt construction into a composePrompt(data) helper that returns the prompt string, move the fetch call to callRecoupGenerate(prompt, recoupApiUrl, recoupApiKey) which performs the POST and throws or returns the parsed JSON, add a normalizeGeneratedText(json) helper that implements the json.text normalization logic (handle string vs array and trimming/quote stripping), and finally have a small mapping function mapToNextResponse(content) that returns the NextResponse with getCorsHeaders and the font/color metadata; update createTextHandler to orchestrate these helpers and keep it under ~50 lines. Ensure helper names (composePrompt, callRecoupGenerate, normalizeGeneratedText, mapToNextResponse) are used so reviewers can find and test each responsibility independently.app/api/content/create/text/route.ts (1)
1-13: UsecreatePrimitiveRoutehere to avoid route wiring drift.This endpoint manually duplicates the OPTIONS/config pattern already centralized in
lib/content/primitives/primitiveRoute.ts. Reusing it keeps all primitive routes aligned.♻️ Proposed refactor
-import { NextResponse } from "next/server"; -import { getCorsHeaders } from "@/lib/networking/getCorsHeaders"; import { createTextHandler } from "@/lib/content/primitives/createTextHandler"; +import { createPrimitiveRoute, dynamic, fetchCache, revalidate } from "@/lib/content/primitives/primitiveRoute"; -export async function OPTIONS() { - return new NextResponse(null, { status: 204, headers: getCorsHeaders() }); -} - -export { createTextHandler as POST }; - -export const dynamic = "force-dynamic"; -export const fetchCache = "force-no-store"; -export const revalidate = 0; +export const { OPTIONS, POST } = createPrimitiveRoute(createTextHandler); +export { dynamic, fetchCache, revalidate };As per coding guidelines: “Extract shared logic into reusable utilities following DRY principle”.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/api/content/create/text/route.ts` around lines 1 - 13, The route duplicates OPTIONS and config wiring; replace the manual export and OPTIONS handler by using the shared helper createPrimitiveRoute so the endpoint inherits standard CORS/options and configs. Locate this file's export of createTextHandler and the manual OPTIONS/exports and refactor to call createPrimitiveRoute with createTextHandler (referencing createPrimitiveRoute and createTextHandler) so the route wiring (OPTIONS, dynamic/fetchCache/revalidate and CORS headers) is centralized in lib/content/primitives/primitiveRoute.ts.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@app/api/content/create/image/route.ts`:
- Around line 1-7: Add JSDoc comments to this module and its primary exports:
place a module-level JSDoc at the top of the file describing the route purpose
(Create image API), accepted payload (referencing createImageBodySchema), and
supported methods; add a brief JSDoc above the handler creation line that
documents createPrimitiveHandler("create-image", createImageBodySchema) and its
validation behavior, and a short JSDoc above the export line for the generated
route exports (OPTIONS, POST) and the re-exported dynamic, fetchCache,
revalidate flags so callers understand their meaning and usage.
- Around line 1-3: The file fails Prettier formatting; run the project's
Prettier formatter (e.g., via npm/yarn script like format or npx prettier
--write) on this route to fix spacing/line breaks and import ordering for the
top-level imports including createPrimitiveHandler, createPrimitiveRoute,
dynamic, fetchCache, revalidate, and createImageBodySchema; ensure the file
matches the repo's Prettier config so CI format checks pass.
- Around line 5-7: Add endpoint-level integration tests for the create-image
route (exports: handler, POST from createPrimitiveRoute) that exercise real auth
behavior instead of mocking validatePrimitiveBody: write tests that hit the POST
endpoint backed by createPrimitiveHandler + createImageBodySchema and cover (1)
success: valid auth and body returns 202; (2) auth errors: missing/invalid
credentials produce 401 and unauthorized account produce 403 by calling the real
validateAuthContext flow; (3) validation error: invalid request body returns 400
from the createImageBodySchema validation; and (4) server error: simulate the
underlying service throwing to get 500; ensure tests only mock external services
(e.g., storage/service clients) but not validateAuthContext for auth-path
assertions and use the same route exports (handler/POST) to exercise endpoint
semantics.
In `@app/api/content/create/text/route.ts`:
- Around line 5-10: Add JSDoc comments to this API route by documenting the
exported OPTIONS handler and the POST export alias: add a JSDoc block above the
OPTIONS function describing its purpose (CORS preflight response), parameters
(none) and return type, and add a JSDoc block above the export line for
createTextHandler (or the createTextHandler function definition if present)
describing the POST handler’s purpose, expected request body, responses, and any
errors; ensure the JSDoc uses standard tags like `@route`, `@method`, `@returns` and
references the symbols OPTIONS and createTextHandler so the route complies with
the “All API routes should have JSDoc comments” guideline.
In `@app/api/content/create/upscale/route.ts`:
- Around line 1-7: Add concise route-level JSDoc comments explaining the API
route purpose and the handlers: document the module at top and annotate the main
handler creation/export lines (createPrimitiveHandler("create-upscale",
createUpscaleBodySchema), the exported handler object via createPrimitiveRoute,
and the exported members OPTIONS and POST) with a short description of the route
behavior, accepted request shape (referencing createUpscaleBodySchema), and any
important response/side-effect notes; place the JSDoc directly above the
handler/exports so route documentation tools pick it up (also include brief
one-line comments for re-exported symbols dynamic, fetchCache, revalidate if
needed).
In `@app/api/content/create/video/route.ts`:
- Around line 1-7: Add a route-level JSDoc block above the endpoint exports in
this module describing the API route purpose and exported handlers: document
that this file defines the "create-video" primitive handler (handler) and
exports the OPTIONS and POST endpoints via createPrimitiveRoute(handler), and
include tags for parameters, request/response shape (based on
createVideoBodySchema), and any middleware/behavior (dynamic, fetchCache,
revalidate) so the route adheres to the project JSDoc standard.
- Around line 1-7: Add route-level tests for the create-video endpoint: write
tests that exercise the exported POST and OPTIONS handlers (created via
createPrimitiveRoute(handler) where handler comes from
createPrimitiveHandler("create-video", createVideoBodySchema)), asserting a
valid request returns HTTP 202 with a JSON body containing runId, malformed
payloads return HTTP 400, authentication failure path is covered if the route
enforces auth, and OPTIONS responses include appropriate CORS headers; follow
the structure and assertions used in the coding-agent route tests (e.g., request
construction, header checks, and status/body expectations) to implement success,
error, auth, and CORS/OPTIONS test cases.
In `@lib/content/primitives/createTextHandler.ts`:
- Around line 32-40: The fetch to `${recoupApiUrl}/api/chat/generate` in
createTextHandler.ts has no timeout; wrap the request in an AbortController
(pass controller.signal to fetch), start a timer (e.g., 5–15s) that calls
controller.abort() on timeout, and clear the timer when the request completes;
catch the AbortError separately to return/log a clear timeout error and rethrow
or return an appropriate response. Update the fetch invocation that uses
recoupApiKey/ prompt to include the signal and ensure the timeout is cleared to
avoid leaks.
In `@lib/content/primitives/primitiveRoute.ts`:
- Line 10: The OPTIONS handler currently returns global wildcard CORS via the
OPTIONS constant and getCorsHeaders(), which is unsafe for endpoints accepting
Authorization/x-api-key; replace this by checking the incoming request Origin
header against an explicit allowlist and only returning that Origin (or no CORS)
when it matches. Update the OPTIONS constant to read
request.headers.get('origin'), validate it against a configured allowlist, and
return a NextResponse with Access-Control-Allow-Origin set to the matched origin
(and appropriate allow headers) or a 204 with no CORS; alternatively implement a
helper (e.g., getRestrictedCorsHeaders or extend getCorsHeaders to accept an
origin) and use that in OPTIONS and in other responses for authenticated
primitive endpoints. Ensure the symbols to change are OPTIONS and getCorsHeaders
so all authenticated routes stop using the wildcard.
In `@lib/content/primitives/schemas.ts`:
- Around line 17-18: Update the numeric timing/size fields in the schema to
reject negative values by adding a lower-bound (e.g., use z.number().min(0) or
.nonnegative()) to audio_start_seconds and audio_duration_seconds and to the
other numeric timing/size fields mentioned in the review (the fields on the
later numeric lines referenced), so each such field uses z.number().min(0) (and
include a clear custom error message if desired) instead of
z.number().optional(); ensure optionality is preserved if needed (e.g.,
z.number().min(0).optional()) so downstream payloads cannot contain negative
timing/size values.
- Line 5: Remove the artist_account_id property from the request body Zod
schemas in lib/content/primitives/schemas.ts (the fields named artist_account_id
in the relevant schema declarations) and update callers to stop expecting it;
instead, derive the account ID from the authentication context (e.g.,
request.user/account in your auth middleware) and inject that value server-side
into the payloads used by functions that create/update artists. Ensure any
validation or Zod schema references no longer include artist_account_id and that
server-side handlers (where these schemas are used) read the authenticated
account ID and attach it to the model before persisting or authorizing
operations.
---
Nitpick comments:
In `@app/api/content/create/text/route.ts`:
- Around line 1-13: The route duplicates OPTIONS and config wiring; replace the
manual export and OPTIONS handler by using the shared helper
createPrimitiveRoute so the endpoint inherits standard CORS/options and configs.
Locate this file's export of createTextHandler and the manual OPTIONS/exports
and refactor to call createPrimitiveRoute with createTextHandler (referencing
createPrimitiveRoute and createTextHandler) so the route wiring (OPTIONS,
dynamic/fetchCache/revalidate and CORS headers) is centralized in
lib/content/primitives/primitiveRoute.ts.
In `@lib/content/primitives/createTextHandler.ts`:
- Around line 11-80: Split the large createTextHandler into smaller helpers:
extract validation into keep using validatePrimitiveBody with
createTextBodySchema, move prompt construction into a composePrompt(data) helper
that returns the prompt string, move the fetch call to
callRecoupGenerate(prompt, recoupApiUrl, recoupApiKey) which performs the POST
and throws or returns the parsed JSON, add a normalizeGeneratedText(json) helper
that implements the json.text normalization logic (handle string vs array and
trimming/quote stripping), and finally have a small mapping function
mapToNextResponse(content) that returns the NextResponse with getCorsHeaders and
the font/color metadata; update createTextHandler to orchestrate these helpers
and keep it under ~50 lines. Ensure helper names (composePrompt,
callRecoupGenerate, normalizeGeneratedText, mapToNextResponse) are used so
reviewers can find and test each responsibility independently.
🪄 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: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: d59a6f13-51cf-4b51-a0da-e16caa5e3b1d
⛔ Files ignored due to path filters (3)
lib/content/primitives/__tests__/handlePrimitiveTrigger.test.tsis excluded by!**/*.test.*,!**/__tests__/**and included bylib/**lib/content/primitives/__tests__/schemas.test.tsis excluded by!**/*.test.*,!**/__tests__/**and included bylib/**lib/content/primitives/__tests__/validatePrimitiveBody.test.tsis excluded by!**/*.test.*,!**/__tests__/**and included bylib/**
📒 Files selected for processing (12)
app/api/content/create/audio/route.tsapp/api/content/create/image/route.tsapp/api/content/create/render/route.tsapp/api/content/create/text/route.tsapp/api/content/create/upscale/route.tsapp/api/content/create/video/route.tslib/content/primitives/createTextHandler.tslib/content/primitives/handlePrimitiveTrigger.tslib/content/primitives/primitiveRoute.tslib/content/primitives/schemas.tslib/content/primitives/validatePrimitiveBody.tslib/trigger/triggerPrimitive.ts
| import { createPrimitiveHandler } from "@/lib/content/primitives/handlePrimitiveTrigger"; | ||
| import { createPrimitiveRoute, dynamic, fetchCache, revalidate } from "@/lib/content/primitives/primitiveRoute"; | ||
| import { createImageBodySchema } from "@/lib/content/primitives/schemas"; | ||
|
|
||
| const handler = createPrimitiveHandler("create-image", createImageBodySchema); | ||
| export const { OPTIONS, POST } = createPrimitiveRoute(handler); | ||
| export { dynamic, fetchCache, revalidate }; |
There was a problem hiding this comment.
Add JSDoc comments to this route module.
This new API route does not include JSDoc comments.
Based on learnings: “Applies to app/api/**/route.ts : All API routes should have JSDoc comments”.
🧰 Tools
🪛 GitHub Actions: Format Check
[warning] 1-1: Prettier --check reported code style issues in this file.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@app/api/content/create/image/route.ts` around lines 1 - 7, Add JSDoc comments
to this module and its primary exports: place a module-level JSDoc at the top of
the file describing the route purpose (Create image API), accepted payload
(referencing createImageBodySchema), and supported methods; add a brief JSDoc
above the handler creation line that documents
createPrimitiveHandler("create-image", createImageBodySchema) and its validation
behavior, and a short JSDoc above the export line for the generated route
exports (OPTIONS, POST) and the re-exported dynamic, fetchCache, revalidate
flags so callers understand their meaning and usage.
| const handler = createPrimitiveHandler("create-image", createImageBodySchema); | ||
| export const { OPTIONS, POST } = createPrimitiveRoute(handler); | ||
| export { dynamic, fetchCache, revalidate }; |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Verify tests for new primitive create endpoints and presence of success/error assertions.
# Expected: at least one test file covers /api/content/create/image and error cases.
rg -n --iglob '*.{test,spec}.{ts,tsx,js,jsx}' \
-e '/api/content/create/(image|video|audio|render|upscale|text)' \
-e 'create-(image|video|audio|render|upscale)' \
-C2
rg -n --iglob '*.{test,spec}.{ts,tsx,js,jsx}' \
-e '\b(200|202|400|401|403|422|500)\b' \
-e 'toThrow|rejects|error' \
-C1Repository: recoupable/api
Length of output: 50371
🏁 Script executed:
# Check the test file that was found
wc -l lib/content/primitives/__tests__/handlePrimitiveTrigger.test.tsRepository: recoupable/api
Length of output: 124
🏁 Script executed:
# Read the complete test file to assess coverage
cat -n lib/content/primitives/__tests__/handlePrimitiveTrigger.test.tsRepository: recoupable/api
Length of output: 3012
🏁 Script executed:
# Check the createPrimitiveHandler and createPrimitiveRoute implementations
find . -name "*.ts" -path "*/lib/content/primitives/*" ! -path "*/__tests__/*" | head -10Repository: recoupable/api
Length of output: 283
🏁 Script executed:
# Look for the route handler implementation
cat -n app/api/content/create/image/route.tsRepository: recoupable/api
Length of output: 571
🏁 Script executed:
# Check the validatePrimitiveBody to see what auth errors it can return
cat -n lib/content/primitives/validatePrimitiveBody.tsRepository: recoupable/api
Length of output: 1494
🏁 Script executed:
# Check the primitiveRoute implementation
cat -n lib/content/primitives/primitiveRoute.tsRepository: recoupable/api
Length of output: 883
🏁 Script executed:
# Search for any endpoint-level integration tests for the image creation route
find . -name "*.test.ts" -o -name "*.spec.ts" | xargs grep -l "api/content/create/image" 2>/dev/null | head -5Repository: recoupable/api
Length of output: 40
🏁 Script executed:
# Check what error codes are tested in the primitive handler test
rg "expect.*status.*toBe" lib/content/primitives/__tests__/handlePrimitiveTrigger.test.tsRepository: recoupable/api
Length of output: 217
Tests exist for the handler abstraction but lack endpoint-level coverage for authentication error paths.
The existing tests in lib/content/primitives/__tests__/handlePrimitiveTrigger.test.ts cover the handler's success (202) and some error paths (400, 500), but mock validatePrimitiveBody, which means the actual authentication error paths (401, 403) that validateAuthContext() produces are never exercised. No endpoint-level integration tests for this route were found.
Per the coding guidelines, all API endpoints should have tests covering success and error paths. Add integration tests covering:
- Success: 202 response with valid auth and data
- Auth errors: 401 (missing/invalid credentials), 403 (unauthorized account access)
- Validation errors: 400 (invalid request body)
- Server errors: 500 (trigger service failure)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@app/api/content/create/image/route.ts` around lines 5 - 7, Add endpoint-level
integration tests for the create-image route (exports: handler, POST from
createPrimitiveRoute) that exercise real auth behavior instead of mocking
validatePrimitiveBody: write tests that hit the POST endpoint backed by
createPrimitiveHandler + createImageBodySchema and cover (1) success: valid auth
and body returns 202; (2) auth errors: missing/invalid credentials produce 401
and unauthorized account produce 403 by calling the real validateAuthContext
flow; (3) validation error: invalid request body returns 400 from the
createImageBodySchema validation; and (4) server error: simulate the underlying
service throwing to get 500; ensure tests only mock external services (e.g.,
storage/service clients) but not validateAuthContext for auth-path assertions
and use the same route exports (handler/POST) to exercise endpoint semantics.
| import { createPrimitiveHandler } from "@/lib/content/primitives/handlePrimitiveTrigger"; | ||
| import { createPrimitiveRoute, dynamic, fetchCache, revalidate } from "@/lib/content/primitives/primitiveRoute"; | ||
| import { createVideoBodySchema } from "@/lib/content/primitives/schemas"; | ||
|
|
||
| const handler = createPrimitiveHandler("create-video", createVideoBodySchema); | ||
| export const { OPTIONS, POST } = createPrimitiveRoute(handler); | ||
| export { dynamic, fetchCache, revalidate }; |
There was a problem hiding this comment.
Add a route-level JSDoc block to this API module.
This route file is missing JSDoc documentation above its endpoint exports.
Based on learnings: "Applies to app/api/**/route.ts : All API routes should have JSDoc comments".
🧰 Tools
🪛 GitHub Actions: Format Check
[warning] 1-1: Prettier --check reported code style issues in this file.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@app/api/content/create/video/route.ts` around lines 1 - 7, Add a route-level
JSDoc block above the endpoint exports in this module describing the API route
purpose and exported handlers: document that this file defines the
"create-video" primitive handler (handler) and exports the OPTIONS and POST
endpoints via createPrimitiveRoute(handler), and include tags for parameters,
request/response shape (based on createVideoBodySchema), and any
middleware/behavior (dynamic, fetchCache, revalidate) so the route adheres to
the project JSDoc standard.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "Looking for tests that reference new primitive endpoints..."
rg -n --type=ts --type=tsx --glob '*{test,spec}*' \
'/api/content/create/(video|audio|render|image|upscale|text)|create-(video|audio|render|image|upscale|text)' .
echo "Listing route modules for quick parity checks..."
fd -a 'route.ts$' app/api/content/createRepository: recoupable/api
Length of output: 162
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== Searching for test files mentioning video creation endpoint ==="
rg -n --type=ts '(create[_-]?video|/api/content/create/video)' . --glob='**/*.{test,spec}.ts' || echo "No test files found matching pattern"
echo ""
echo "=== Listing all test files in content/create directory ==="
fd -t f '\.test\.ts$|\.spec\.ts$' 'app/api/content/create' || echo "No test files found in this directory"
echo ""
echo "=== Checking for general test patterns near route files ==="
fd -t f 'route\.ts$' 'app/api/content/create' | while read -r route_file; do
dir=$(dirname "$route_file")
echo "Route: $route_file"
echo " Test files in same directory:"
find "$dir" -maxdepth 1 -name '*.test.ts' -o -name '*.spec.ts' 2>/dev/null | sed 's/^/ /' || echo " (none found)"
done
echo ""
echo "=== Checking if tests exist in separate __tests__ directory ==="
fd -t d '__tests__' 'app/api/content' || echo "No __tests__ directory found"Repository: recoupable/api
Length of output: 883
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== Searching for ANY test files in app/api ==="
fd -t f '\.test\.ts$|\.spec\.ts$' 'app/api' | head -20
echo ""
echo "=== Checking for test directory structure patterns ==="
fd -t d 'test|spec|__tests__' 'app/api' | head -20
echo ""
echo "=== Checking if there's a tests directory at project root ==="
ls -la tests/ 2>/dev/null || echo "No tests directory at root"
echo ""
echo "=== Looking for any test references to primitive endpoints ==="
rg -l 'createVideoBodySchema|create-video|createPrimitiveHandler' . --type=ts --type=js 2>/dev/null | grep -E '\.(test|spec)\.' || echo "No test files reference these primitives"Repository: recoupable/api
Length of output: 784
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== Checking scope of primitive module tests ==="
head -50 ./lib/content/primitives/__tests__/schemas.test.ts
echo ""
echo "---"
echo ""
head -50 ./lib/content/primitives/__tests__/handlePrimitiveTrigger.test.ts
echo ""
echo "=== Checking structure of a route test example ==="
head -60 ./app/api/coding-agent/__tests__/route.test.ts
echo ""
echo "=== Searching for route-level tests that call the endpoint ==="
rg -A 5 'POST.*create.*video|createVideoBodySchema' ./app/api/content/create --type=ts || echo "No route tests found"Repository: recoupable/api
Length of output: 5317
Add dedicated route-level tests for this endpoint covering success and error paths.
While tests exist for the underlying handler and schemas (./lib/content/primitives/__tests__/), dedicated route-level tests for the /api/content/create/video endpoint are missing. Add tests in app/api/content/create/video/__tests__/route.test.ts covering:
- Valid requests returning 202 with
runId - Invalid payloads returning 400
- Authentication errors (if applicable)
- CORS headers and OPTIONS responses
Follow the pattern in app/api/coding-agent/__tests__/route.test.ts for reference.
🧰 Tools
🪛 GitHub Actions: Format Check
[warning] 1-1: Prettier --check reported code style issues in this file.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@app/api/content/create/video/route.ts` around lines 1 - 7, Add route-level
tests for the create-video endpoint: write tests that exercise the exported POST
and OPTIONS handlers (created via createPrimitiveRoute(handler) where handler
comes from createPrimitiveHandler("create-video", createVideoBodySchema)),
asserting a valid request returns HTTP 202 with a JSON body containing runId,
malformed payloads return HTTP 400, authentication failure path is covered if
the route enforces auth, and OPTIONS responses include appropriate CORS headers;
follow the structure and assertions used in the coding-agent route tests (e.g.,
request construction, header checks, and status/body expectations) to implement
success, error, auth, and CORS/OPTIONS test cases.
| * Provides CORS OPTIONS, the POST handler, and Next.js dynamic config. | ||
| */ | ||
| export function createPrimitiveRoute(handler: (req: NextRequest) => Promise<NextResponse>) { | ||
| const OPTIONS = () => new NextResponse(null, { status: 204, headers: getCorsHeaders() }); |
There was a problem hiding this comment.
Restrict wildcard CORS on authenticated primitive endpoints.
Line 10 uses global wildcard CORS headers. Since these routes accept Authorization / x-api-key, this broadens cross-origin read access for authenticated calls more than necessary. Prefer an explicit origin allowlist for these endpoints.
🔧 Suggested direction
- const OPTIONS = () => new NextResponse(null, { status: 204, headers: getCorsHeaders() });
+ const OPTIONS = (request: NextRequest) =>
+ new NextResponse(null, {
+ status: 204,
+ headers: getCorsHeaders(/* origin-aware allowlist */),
+ });🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@lib/content/primitives/primitiveRoute.ts` at line 10, The OPTIONS handler
currently returns global wildcard CORS via the OPTIONS constant and
getCorsHeaders(), which is unsafe for endpoints accepting
Authorization/x-api-key; replace this by checking the incoming request Origin
header against an explicit allowlist and only returning that Origin (or no CORS)
when it matches. Update the OPTIONS constant to read
request.headers.get('origin'), validate it against a configured allowlist, and
return a NextResponse with Access-Control-Allow-Origin set to the matched origin
(and appropriate allow headers) or a 204 with no CORS; alternatively implement a
helper (e.g., getRestrictedCorsHeaders or extend getCorsHeaders to accept an
origin) and use that in OPTIONS and in other responses for authenticated
primitive endpoints. Ensure the symbols to change are OPTIONS and getCorsHeaders
so all authenticated routes stop using the wildcard.
Next.js 16 Turbopack requires dynamic, fetchCache, and revalidate to be declared directly in route files — they cannot be re-exported from shared modules. Moved these exports inline into each route file. Made-with: Cursor
- Add JSDoc to all 6 route files per API route convention - Add numeric bounds (.nonnegative/.positive) to timing fields in schemas - Add 30s AbortController timeout to text handler's upstream fetch - Run prettier to fix formatting Made-with: Cursor
Image, video, audio, text, and upscale now call fal.ai directly from the API handler — no Trigger.dev task needed. This means they work on any Vercel deployment (including previews) without needing task infrastructure. Render still triggers a Trigger.dev task because it needs ffmpeg. - Added @fal-ai/client dependency - Created inline handlers for image, video, audio, upscale - Deleted handlePrimitiveTrigger factory (no longer needed) - Deleted primitiveRoute helper (no longer imported) - Updated all route files to use inline handlers Made-with: Cursor
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (4)
lib/content/primitives/createTextHandler.ts (1)
14-96: RefactorcreateTextHandlerinto smaller helpers (currently too large and multi-purpose).
createTextHandleris handling too many responsibilities in one block (validation flow, prompt building, upstream call, parsing/normalization, response shaping). Splitting this into private helpers will improve maintainability and testability.♻️ Proposed refactor
export async function createTextHandler(request: NextRequest): Promise<NextResponse> { const validated = await validatePrimitiveBody(request, createTextBodySchema); if (validated instanceof NextResponse) return validated; const { data } = validated; try { const recoupApiUrl = process.env.RECOUP_API_URL ?? "https://recoup-api.vercel.app"; const recoupApiKey = process.env.RECOUP_API_KEY; if (!recoupApiKey) { return NextResponse.json( { status: "error", error: "RECOUP_API_KEY is not configured" }, { status: 500, headers: getCorsHeaders() }, ); } - const prompt = `Generate ONE short on-screen text for a social media video. -Song or theme: "${data.song}" -Length: ${data.length} -Return ONLY the text, nothing else. No quotes.`; + const prompt = buildPrompt(data.song, data.length); const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), 30_000); try { - const response = await fetch(`${recoupApiUrl}/api/chat/generate`, { - method: "POST", - headers: { "Content-Type": "application/json", "x-api-key": recoupApiKey }, - body: JSON.stringify({ - prompt, - model: "google/gemini-2.5-flash", - excludeTools: ["create_task"], - }), - signal: controller.signal, - }); + const response = await requestGeneratedText(recoupApiUrl, recoupApiKey, prompt, controller.signal); if (!response.ok) { return NextResponse.json( { status: "error", error: `Text generation failed: ${response.status}` }, { status: 502, headers: getCorsHeaders() }, ); } const json = (await response.json()) as { text?: string | Array<{ type: string; text?: string }>; }; - let content: string; - if (typeof json.text === "string") { - content = json.text.trim(); - } else if (Array.isArray(json.text)) { - content = json.text - .filter(p => p.type === "text" && p.text) - .map(p => p.text!) - .join("") - .trim(); - } else { - content = ""; - } - - content = content.replace(/^["']|["']$/g, "").trim(); + const content = normalizeGeneratedText(json.text); if (!content) { return NextResponse.json( { status: "error", error: "Text generation returned empty" }, { status: 502, headers: getCorsHeaders() }, ); } @@ } } + +function buildPrompt(song: string, length: string): string { + return `Generate ONE short on-screen text for a social media video. +Song or theme: "${song}" +Length: ${length} +Return ONLY the text, nothing else. No quotes.`; +} + +async function requestGeneratedText( + recoupApiUrl: string, + recoupApiKey: string, + prompt: string, + signal: AbortSignal, +): Promise<Response> { + return fetch(`${recoupApiUrl}/api/chat/generate`, { + method: "POST", + headers: { "Content-Type": "application/json", "x-api-key": recoupApiKey }, + body: JSON.stringify({ + prompt, + model: "google/gemini-2.5-flash", + excludeTools: ["create_task"], + }), + signal, + }); +} + +function normalizeGeneratedText(text: string | Array<{ type: string; text?: string }> | undefined): string { + const content = + typeof text === "string" + ? text.trim() + : Array.isArray(text) + ? text + .filter(p => p.type === "text" && p.text) + .map(p => p.text!) + .join("") + .trim() + : ""; + + return content.replace(/^["']|["']$/g, "").trim(); +}As per coding guidelines: “For domain functions, ensure: Single responsibility per function … Keep functions under 50 lines … DRY … KISS.”
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/content/primitives/createTextHandler.ts` around lines 14 - 96, createTextHandler is doing too much; split it into focused helpers: keep createTextHandler for orchestration (validation via validatePrimitiveBody and early error responses), move prompt construction into buildTextPrompt(data), the upstream fetch into callRecoupGenerate(prompt, recoupApiUrl, recoupApiKey, signal) which returns the parsed JSON, move parsing/normalization into parseGeneratedText(json) to produce a clean content string, and move the final response shaping into shapeTextResponse(content) which returns the object sent in NextResponse.json; update createTextHandler to call these helpers, preserve the existing error/timeout handling (AbortController/clearTimeout) and all existing checks (RECOUP_API_KEY, response.ok, empty content) and reuse symbols validatePrimitiveBody and createTextBodySchema unchanged.lib/content/primitives/createAudioHandler.ts (3)
39-39: Unnecessaryas stringcast on a string literal.
"fal-ai/whisper"is already a string literal; theas stringcast adds noise without value.✂️ Remove redundant cast
- const result = await fal.subscribe("fal-ai/whisper" as string, { + const result = await fal.subscribe("fal-ai/whisper", {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/content/primitives/createAudioHandler.ts` at line 39, The call to fal.subscribe in createAudioHandler.ts unnecessarily casts the string literal "fal-ai/whisper" with as string; remove the redundant cast so the call reads fal.subscribe("fal-ai/whisper", {...}) (locate the invocation of fal.subscribe that assigns to result and drop the unnecessary as string).
54-58: Array index access onchunk.timestampcould be fragile.Accessing
chunk.timestamp[0]andchunk.timestamp[1]assumes the array always has at least two elements. The nullish coalescing (?? 0) provides a fallback, but if the Whisper API ever returns malformed data, this could silently produce incorrect timing values.A defensive check or destructuring with defaults would be slightly more explicit:
🛡️ More defensive timestamp extraction
const segments = (whisperData.chunks ?? []).map(chunk => ({ - start: chunk.timestamp[0] ?? 0, - end: chunk.timestamp[1] ?? 0, + start: Array.isArray(chunk.timestamp) ? (chunk.timestamp[0] ?? 0) : 0, + end: Array.isArray(chunk.timestamp) ? (chunk.timestamp[1] ?? 0) : 0, text: chunk.text?.trim() ?? "", }));🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/content/primitives/createAudioHandler.ts` around lines 54 - 58, The mapping over whisperData.chunks in createAudioHandler.ts currently uses chunk.timestamp[0] and [1] directly which is fragile; update the segment extraction to defensively destructure and validate timestamps (e.g., const [start = 0, end = 0] = Array.isArray(chunk.timestamp) ? chunk.timestamp : []) and ensure start and end are numbers (or skip the chunk) before using them, so segments uses safe defaults or excludes malformed entries when building the segments array.
48-51: Use Zod validation for fal-ai/whisper response shape instead of type casting.The double cast
as unknown as { ... }on lines 48-51 bypasses TypeScript's type checking. Since the codebase already uses Zod extensively for validation, define a schema for the whisper response (e.g.,whisperResponseSchema) and validateresult.dataagainst it. This ensures type safety at runtime and maintains consistency with the existing validation pattern used throughout the codebase.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/content/primitives/createAudioHandler.ts` around lines 48 - 51, Replace the unsafe double-cast for whisperData by defining a Zod schema (e.g., whisperResponseSchema) that matches the fal-ai/whisper response shape (fields: optional text: string and optional chunks: Array<{ timestamp: number[]; text: string }>), then parse/validate result.data with whisperResponseSchema.parse or safeParse inside createAudioHandler (referencing whisperData and result.data) and use the validated type instead of the cast; on parse failure throw or handle the validation error consistently with existing validation patterns in the codebase.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@lib/content/primitives/createAudioHandler.ts`:
- Line 26: The handler currently calls fal.config({ credentials: falKey })
inside createAudioHandler, mutating module-global Fal configuration per request
and risking race conditions; instead, initialize and configure the Fal client
once at module scope (e.g., create a single falClient or call fal.config(...)
once during module initialization guarded by an environment check) and have
createAudioHandler (and the other primitives createImageHandler,
createVideoHandler, createUpscaleHandler) reuse that singleton client; remove
per-request fal.config calls and replace any reference to the local fal config
with the shared falClient to ensure thread-safe, consistent configuration.
- Around line 39-46: createAudioHandler may hang because fal.subscribe is called
without an AbortSignal; add the same 30s timeout pattern used in
createTextHandler by creating an AbortController, pass controller.signal as the
abortSignal option to fal.subscribe, and set a setTimeout to call
controller.abort() after 30_000 ms (clearing the timer after successful
completion or error). Update the call site for fal.subscribe in
createAudioHandler to include abortSignal and ensure any timer is cleared in
both success and catch/finally paths to avoid leaks.
In `@lib/content/primitives/createVideoHandler.ts`:
- Around line 32-51: The handler silently falls back to image-to-video when
data.lipsync is true but data.song_url is missing; add explicit validation at
the start of the create video flow (the function containing the current block
where data.lipsync and data.song_url are read) to detect if data.lipsync &&
!data.song_url and return a 400 error (or throw a validation error) with a clear
message like "song_url is required when lipsync is true"; alternatively,
implement the same dependency as a schema refinement, but if fixing here,
short-circuit before calling fal.subscribe so
fal.subscribe("fal-ai/ltx-2-19b/audio-to-video", ...) is only invoked when
song_url is present.
---
Nitpick comments:
In `@lib/content/primitives/createAudioHandler.ts`:
- Line 39: The call to fal.subscribe in createAudioHandler.ts unnecessarily
casts the string literal "fal-ai/whisper" with as string; remove the redundant
cast so the call reads fal.subscribe("fal-ai/whisper", {...}) (locate the
invocation of fal.subscribe that assigns to result and drop the unnecessary as
string).
- Around line 54-58: The mapping over whisperData.chunks in
createAudioHandler.ts currently uses chunk.timestamp[0] and [1] directly which
is fragile; update the segment extraction to defensively destructure and
validate timestamps (e.g., const [start = 0, end = 0] =
Array.isArray(chunk.timestamp) ? chunk.timestamp : []) and ensure start and end
are numbers (or skip the chunk) before using them, so segments uses safe
defaults or excludes malformed entries when building the segments array.
- Around line 48-51: Replace the unsafe double-cast for whisperData by defining
a Zod schema (e.g., whisperResponseSchema) that matches the fal-ai/whisper
response shape (fields: optional text: string and optional chunks: Array<{
timestamp: number[]; text: string }>), then parse/validate result.data with
whisperResponseSchema.parse or safeParse inside createAudioHandler (referencing
whisperData and result.data) and use the validated type instead of the cast; on
parse failure throw or handle the validation error consistently with existing
validation patterns in the codebase.
In `@lib/content/primitives/createTextHandler.ts`:
- Around line 14-96: createTextHandler is doing too much; split it into focused
helpers: keep createTextHandler for orchestration (validation via
validatePrimitiveBody and early error responses), move prompt construction into
buildTextPrompt(data), the upstream fetch into callRecoupGenerate(prompt,
recoupApiUrl, recoupApiKey, signal) which returns the parsed JSON, move
parsing/normalization into parseGeneratedText(json) to produce a clean content
string, and move the final response shaping into shapeTextResponse(content)
which returns the object sent in NextResponse.json; update createTextHandler to
call these helpers, preserve the existing error/timeout handling
(AbortController/clearTimeout) and all existing checks (RECOUP_API_KEY,
response.ok, empty content) and reuse symbols validatePrimitiveBody and
createTextBodySchema unchanged.
🪄 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: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: f73090b4-9322-494b-9c75-459fb319758e
⛔ Files ignored due to path filters (32)
lib/admins/emails/__tests__/validateGetAdminEmailsQuery.test.tsis excluded by!**/*.test.*,!**/__tests__/**and included bylib/**lib/admins/pr/__tests__/getPrMergedStatusHandler.test.tsis excluded by!**/*.test.*,!**/__tests__/**and included bylib/**lib/agents/content/__tests__/handleContentAgentCallback.test.tsis excluded by!**/*.test.*,!**/__tests__/**and included bylib/**lib/artists/__tests__/createArtistPostHandler.test.tsis excluded by!**/*.test.*,!**/__tests__/**and included bylib/**lib/artists/__tests__/validateCreateArtistBody.test.tsis excluded by!**/*.test.*,!**/__tests__/**and included bylib/**lib/auth/__tests__/validateAuthContext.test.tsis excluded by!**/*.test.*,!**/__tests__/**and included bylib/**lib/chat/__tests__/integration/chatEndToEnd.test.tsis excluded by!**/*.test.*,!**/__tests__/**and included bylib/**lib/chats/__tests__/createChatHandler.test.tsis excluded by!**/*.test.*,!**/__tests__/**and included bylib/**lib/coding-agent/__tests__/handleGitHubWebhook.test.tsis excluded by!**/*.test.*,!**/__tests__/**and included bylib/**lib/coding-agent/__tests__/onMergeTestToMainAction.test.tsis excluded by!**/*.test.*,!**/__tests__/**and included bylib/**lib/content/__tests__/validateCreateContentBody.test.tsis excluded by!**/*.test.*,!**/__tests__/**and included bylib/**lib/content/primitives/__tests__/validatePrimitiveBody.test.tsis excluded by!**/*.test.*,!**/__tests__/**and included bylib/**lib/evals/callChatFunctions.tsis excluded by!**/evals/**and included bylib/**lib/evals/callChatFunctionsWithResult.tsis excluded by!**/evals/**and included bylib/**lib/evals/createToolsCalledScorer.tsis excluded by!**/evals/**and included bylib/**lib/evals/extractTextFromResult.tsis excluded by!**/evals/**and included bylib/**lib/evals/extractTextResultFromSteps.tsis excluded by!**/evals/**and included bylib/**lib/evals/getCatalogSongsCountExpected.tsis excluded by!**/evals/**and included bylib/**lib/evals/getSpotifyFollowersExpected.tsis excluded by!**/evals/**and included bylib/**lib/evals/scorers/CatalogAvailability.tsis excluded by!**/evals/**and included bylib/**lib/evals/scorers/QuestionAnswered.tsis excluded by!**/evals/**and included bylib/**lib/evals/scorers/ToolsCalled.tsis excluded by!**/evals/**and included bylib/**lib/flamingo/__tests__/getFlamingoPresetsHandler.test.tsis excluded by!**/*.test.*,!**/__tests__/**and included bylib/**lib/github/__tests__/createOrUpdateFileContent.test.tsis excluded by!**/*.test.*,!**/__tests__/**and included bylib/**lib/notifications/__tests__/createNotificationHandler.test.tsis excluded by!**/*.test.*,!**/__tests__/**and included bylib/**lib/notifications/__tests__/validateCreateNotificationBody.test.tsis excluded by!**/*.test.*,!**/__tests__/**and included bylib/**lib/tasks/__tests__/enrichTaskWithTriggerInfo.test.tsis excluded by!**/*.test.*,!**/__tests__/**and included bylib/**lib/tasks/__tests__/getTaskRunHandler.test.tsis excluded by!**/*.test.*,!**/__tests__/**and included bylib/**lib/tasks/__tests__/validateGetTaskRunQuery.test.tsis excluded by!**/*.test.*,!**/__tests__/**and included bylib/**lib/tasks/__tests__/validateGetTasksQuery.test.tsis excluded by!**/*.test.*,!**/__tests__/**and included bylib/**package.jsonis excluded by none and included by nonepnpm-lock.yamlis excluded by!**/pnpm-lock.yamland included by none
📒 Files selected for processing (71)
app/api/accounts/[id]/route.tsapp/api/admins/coding/slack/route.tsapp/api/admins/privy/route.tsapp/api/content/create/audio/route.tsapp/api/content/create/image/route.tsapp/api/content/create/render/route.tsapp/api/content/create/text/route.tsapp/api/content/create/upscale/route.tsapp/api/content/create/video/route.tsapp/api/songs/analyze/presets/route.tsapp/api/transcribe/route.tslib/admins/pr/getPrStatusHandler.tslib/admins/privy/countNewAccounts.tslib/admins/privy/fetchPrivyLogins.tslib/admins/privy/getCutoffMs.tslib/admins/privy/getLatestVerifiedAt.tslib/admins/privy/toMs.tslib/ai/getModel.tslib/ai/isEmbedModel.tslib/catalog/formatCatalogSongsAsCSV.tslib/catalog/getCatalogDataAsCSV.tslib/catalog/getCatalogSongs.tslib/catalog/getCatalogs.tslib/chat/toolChains/getPrepareStepResult.tslib/chats/processCompactChatRequest.tslib/coding-agent/encodeGitHubThreadId.tslib/coding-agent/handleMergeSuccess.tslib/coding-agent/parseMergeActionId.tslib/coding-agent/parseMergeTestToMainActionId.tslib/composio/getCallbackUrl.tslib/content/contentTemplates.tslib/content/getArtistContentReadiness.tslib/content/getArtistFileTree.tslib/content/getArtistRootPrefix.tslib/content/getContentValidateHandler.tslib/content/isCompletedRun.tslib/content/persistCreateContentRunVideo.tslib/content/primitives/createAudioHandler.tslib/content/primitives/createImageHandler.tslib/content/primitives/createRenderHandler.tslib/content/primitives/createTextHandler.tslib/content/primitives/createUpscaleHandler.tslib/content/primitives/createVideoHandler.tslib/content/primitives/schemas.tslib/content/primitives/validatePrimitiveBody.tslib/content/validateGetContentEstimateQuery.tslib/content/validateGetContentValidateQuery.tslib/credits/getCreditUsage.tslib/credits/handleChatCredits.tslib/emails/processAndSendEmail.tslib/flamingo/getFlamingoPresetsHandler.tslib/github/expandSubmoduleEntries.tslib/github/getRepoGitModules.tslib/github/resolveSubmodulePath.tslib/mcp/resolveAccountId.tslib/mcp/tools/transcribe/registerTranscribeAudioTool.tslib/prompts/getSystemPrompt.tslib/slack/getBotChannels.tslib/slack/getBotUserId.tslib/slack/getSlackUserInfo.tslib/spotify/getSpotifyFollowers.tslib/supabase/account_artist_ids/getAccountArtistIds.tslib/supabase/account_workspace_ids/getAccountWorkspaceIds.tslib/supabase/files/createFileRecord.tslib/supabase/song_artists/insertSongArtists.tslib/supabase/storage/uploadFileByKey.tslib/transcribe/processAudioTranscription.tslib/transcribe/saveAudioToFiles.tslib/transcribe/saveTranscriptToFiles.tslib/transcribe/types.tslib/trigger/triggerPrimitive.ts
✅ Files skipped from review due to trivial changes (59)
- lib/ai/getModel.ts
- lib/slack/getBotChannels.ts
- lib/coding-agent/parseMergeActionId.ts
- lib/catalog/getCatalogDataAsCSV.ts
- lib/composio/getCallbackUrl.ts
- lib/admins/privy/toMs.ts
- lib/chats/processCompactChatRequest.ts
- lib/admins/privy/getLatestVerifiedAt.ts
- lib/slack/getBotUserId.ts
- lib/supabase/files/createFileRecord.ts
- app/api/transcribe/route.ts
- lib/coding-agent/parseMergeTestToMainActionId.ts
- lib/content/validateGetContentValidateQuery.ts
- lib/supabase/song_artists/insertSongArtists.ts
- lib/emails/processAndSendEmail.ts
- lib/transcribe/saveAudioToFiles.ts
- lib/mcp/resolveAccountId.ts
- app/api/accounts/[id]/route.ts
- lib/chat/toolChains/getPrepareStepResult.ts
- lib/coding-agent/handleMergeSuccess.ts
- lib/supabase/account_workspace_ids/getAccountWorkspaceIds.ts
- lib/spotify/getSpotifyFollowers.ts
- lib/catalog/formatCatalogSongsAsCSV.ts
- lib/admins/pr/getPrStatusHandler.ts
- lib/content/persistCreateContentRunVideo.ts
- lib/content/getArtistContentReadiness.ts
- lib/content/getContentValidateHandler.ts
- lib/transcribe/saveTranscriptToFiles.ts
- lib/content/validateGetContentEstimateQuery.ts
- lib/github/resolveSubmodulePath.ts
- lib/catalog/getCatalogs.ts
- app/api/songs/analyze/presets/route.ts
- lib/content/contentTemplates.ts
- lib/transcribe/processAudioTranscription.ts
- lib/supabase/storage/uploadFileByKey.ts
- lib/github/getRepoGitModules.ts
- lib/admins/privy/fetchPrivyLogins.ts
- app/api/admins/coding/slack/route.ts
- lib/credits/getCreditUsage.ts
- lib/transcribe/types.ts
- lib/content/getArtistFileTree.ts
- lib/prompts/getSystemPrompt.ts
- lib/coding-agent/encodeGitHubThreadId.ts
- lib/admins/privy/countNewAccounts.ts
- lib/credits/handleChatCredits.ts
- lib/content/isCompletedRun.ts
- lib/supabase/account_artist_ids/getAccountArtistIds.ts
- lib/admins/privy/getCutoffMs.ts
- lib/github/expandSubmoduleEntries.ts
- lib/slack/getSlackUserInfo.ts
- app/api/admins/privy/route.ts
- lib/catalog/getCatalogSongs.ts
- lib/content/getArtistRootPrefix.ts
- lib/mcp/tools/transcribe/registerTranscribeAudioTool.ts
- lib/flamingo/getFlamingoPresetsHandler.ts
- app/api/content/create/audio/route.ts
- lib/trigger/triggerPrimitive.ts
- lib/ai/isEmbedModel.ts
- lib/content/primitives/schemas.ts
🚧 Files skipped from review as they are similar to previous changes (2)
- lib/content/primitives/validatePrimitiveBody.ts
- app/api/content/create/text/route.ts
| const result = await fal.subscribe("fal-ai/whisper" as string, { | ||
| input: { | ||
| audio_url: songUrl, | ||
| task: "transcribe", | ||
| chunk_level: "word", | ||
| language: "en", | ||
| }, | ||
| }); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Check how createTextHandler implements timeout for consistency
ast-grep --pattern 'AbortController'Repository: recoupable/api
Length of output: 318
🏁 Script executed:
#!/bin/bash
# Get the full createAudioHandler.ts to see complete implementation
cat -n lib/content/primitives/createAudioHandler.tsRepository: recoupable/api
Length of output: 2966
🏁 Script executed:
#!/bin/bash
# Get the full createTextHandler.ts to see how timeout is implemented
cat -n lib/content/primitives/createTextHandler.tsRepository: recoupable/api
Length of output: 3996
🏁 Script executed:
#!/bin/bash
# Search for fal.subscribe usages to understand its API and timeout handling
rg "fal\.subscribe" -A 10 -B 2Repository: recoupable/api
Length of output: 5146
🏁 Script executed:
#!/bin/bash
# Check if any fal.subscribe calls use signal parameter in the codebase
rg "fal\.subscribe.*signal"Repository: recoupable/api
Length of output: 40
🏁 Script executed:
#!/bin/bash
# Check fal package.json to understand the fal-ai/client library version
rg '"@fal-ai/client"' -A 2 -B 2 package.jsonRepository: recoupable/api
Length of output: 231
🌐 Web query:
fal.subscribe API documentation signal AbortController timeout
💡 Result:
The fal.subscribe API from the @fal-ai/client JavaScript library supports AbortController via AbortSignal for request cancellation and AbortSignal.timeout for client-side timeouts. This feature was added in version 1.2.0 (November 2024). ## Usage fal.subscribe accepts RunOptions (shared with fal.run), which includes: - abortSignal?: AbortSignal - Pass AbortController.signal to cancel the request. - startTimeout?: number - Server-side timeout in seconds (sent as x-fal-request-timeout header). - timeout (client-side): Use AbortSignal.timeout(ms) for client timeout. Example: import { fal } from "@fal-ai/client"; const controller = new AbortController; const timeoutSignal = AbortSignal.timeout(60000); // 60s client timeout const result = await fal.subscribe("fal-ai/flux/dev", { input: { prompt: "a cat" }, abortSignal: AbortSignal.any([controller.signal, timeoutSignal]), // Abort on either startTimeout: 30, // Server start timeout 30s }); controller.abort; // Cancel anytime Cancellation aborts client polling/waiting. For queued requests, use fal.queue.cancel(requestId). Server-side, implement /predict/cancel endpoints to handle GPU cleanup. Official docs: https://docs.fal.ai/reference/client-libraries/javascript/types.common (RunOptions), https://fal-ai.github.io/fal-js/reference/interfaces/FalClient.html (FalClient.subscribe), GitHub issue confirming support: fal-ai/fal-js#84.
Citations:
- 1: feature request: public way to stop fal.run, fal.subscribe fal-ai/fal-js#84
- 2: https://fal-ai.github.io/fal-js/reference/interfaces/FalClient.html
- 3: https://registry.npmjs.org/%40fal-ai%2Fclient
- 4: https://docs.fal.ai/reference/client-libraries/javascript/types.common
- 5: https://docs.fal.ai/api-reference/client-libraries/javascript/queue
- 6: https://docs.fal.ai/documentation/development/handle-cancellations
- 7: https://docs.fal.ai/documentation/development/calling-your-endpoints
- 8: https://docs.fal.ai/clients/javascript
- 9: https://docs.fal.ai/reference/client-libraries/javascript
Add timeout to prevent indefinite hangs from fal.ai Whisper calls.
createAudioHandler lacks timeout protection while createTextHandler uses a 30-second timeout. If the Whisper transcription hangs, the request blocks indefinitely. The @fal-ai/client (v1.9.5) supports AbortSignal for request cancellation via the abortSignal parameter.
⏱️ Suggested: add timeout consistency with other handlers
+ const timeoutSignal = AbortSignal.timeout(30_000); // 30s timeout to match createTextHandler
+
const result = await fal.subscribe("fal-ai/whisper" as string, {
input: {
audio_url: songUrl,
task: "transcribe",
chunk_level: "word",
language: "en",
},
+ abortSignal: timeoutSignal,
});🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@lib/content/primitives/createAudioHandler.ts` around lines 39 - 46,
createAudioHandler may hang because fal.subscribe is called without an
AbortSignal; add the same 30s timeout pattern used in createTextHandler by
creating an AbortController, pass controller.signal as the abortSignal option to
fal.subscribe, and set a setTimeout to call controller.abort() after 30_000 ms
(clearing the timer after successful completion or error). Update the call site
for fal.subscribe in createAudioHandler to include abortSignal and ensure any
timer is cleared in both success and catch/finally paths to avoid leaks.
| if (data.lipsync && data.song_url) { | ||
| const result = await fal.subscribe("fal-ai/ltx-2-19b/audio-to-video" as string, { | ||
| input: { | ||
| image_url: data.image_url, | ||
| audio_url: data.song_url, | ||
| prompt: data.motion_prompt ?? "person staring at camera, subtle movement", | ||
| }, | ||
| }); | ||
| const resultData = result.data as Record<string, unknown>; | ||
| videoUrl = (resultData?.video as Record<string, unknown>)?.url as string | undefined; | ||
| } else { | ||
| const result = await fal.subscribe("fal-ai/veo3.1/fast/image-to-video" as string, { | ||
| input: { | ||
| image_url: data.image_url, | ||
| prompt: data.motion_prompt ?? "nearly still, only natural breathing", | ||
| }, | ||
| }); | ||
| const resultData = result.data as Record<string, unknown>; | ||
| videoUrl = (resultData?.video as Record<string, unknown>)?.url as string | undefined; | ||
| } |
There was a problem hiding this comment.
Consider explicit validation when lipsync=true but song_url is missing.
The current logic silently falls through to the image-to-video model when lipsync is true but song_url is absent. This could surprise callers who expect lipsync behavior but receive a non-lipsync video without any error indication.
Per the schema (context snippet 2), there's no .refine() enforcing this dependency. Consider either:
- Adding a schema refinement to require
song_urlwhenlipsync=true - Returning a 400 error in the handler when
lipsync && !song_url
🛡️ Option: Add validation in handler
try {
const { data } = validated;
let videoUrl: string | undefined;
+ if (data.lipsync && !data.song_url) {
+ return NextResponse.json(
+ { status: "error", error: "song_url is required when lipsync is enabled" },
+ { status: 400, headers: getCorsHeaders() },
+ );
+ }
+
if (data.lipsync && data.song_url) {📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| if (data.lipsync && data.song_url) { | |
| const result = await fal.subscribe("fal-ai/ltx-2-19b/audio-to-video" as string, { | |
| input: { | |
| image_url: data.image_url, | |
| audio_url: data.song_url, | |
| prompt: data.motion_prompt ?? "person staring at camera, subtle movement", | |
| }, | |
| }); | |
| const resultData = result.data as Record<string, unknown>; | |
| videoUrl = (resultData?.video as Record<string, unknown>)?.url as string | undefined; | |
| } else { | |
| const result = await fal.subscribe("fal-ai/veo3.1/fast/image-to-video" as string, { | |
| input: { | |
| image_url: data.image_url, | |
| prompt: data.motion_prompt ?? "nearly still, only natural breathing", | |
| }, | |
| }); | |
| const resultData = result.data as Record<string, unknown>; | |
| videoUrl = (resultData?.video as Record<string, unknown>)?.url as string | undefined; | |
| } | |
| if (data.lipsync && !data.song_url) { | |
| return NextResponse.json( | |
| { status: "error", error: "song_url is required when lipsync is enabled" }, | |
| { status: 400, headers: getCorsHeaders() }, | |
| ); | |
| } | |
| if (data.lipsync && data.song_url) { | |
| const result = await fal.subscribe("fal-ai/ltx-2-19b/audio-to-video" as string, { | |
| input: { | |
| image_url: data.image_url, | |
| audio_url: data.song_url, | |
| prompt: data.motion_prompt ?? "person staring at camera, subtle movement", | |
| }, | |
| }); | |
| const resultData = result.data as Record<string, unknown>; | |
| videoUrl = (resultData?.video as Record<string, unknown>)?.url as string | undefined; | |
| } else { | |
| const result = await fal.subscribe("fal-ai/veo3.1/fast/image-to-video" as string, { | |
| input: { | |
| image_url: data.image_url, | |
| prompt: data.motion_prompt ?? "nearly still, only natural breathing", | |
| }, | |
| }); | |
| const resultData = result.data as Record<string, unknown>; | |
| videoUrl = (resultData?.video as Record<string, unknown>)?.url as string | undefined; | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@lib/content/primitives/createVideoHandler.ts` around lines 32 - 51, The
handler silently falls back to image-to-video when data.lipsync is true but
data.song_url is missing; add explicit validation at the start of the create
video flow (the function containing the current block where data.lipsync and
data.song_url are read) to detect if data.lipsync && !data.song_url and return a
400 error (or throw a validation error) with a clear message like "song_url is
required when lipsync is true"; alternatively, implement the same dependency as
a schema refinement, but if fixing here, short-circuit before calling
fal.subscribe so fal.subscribe("fal-ai/ltx-2-19b/audio-to-video", ...) is only
invoked when song_url is present.
Move auth out of validatePrimitiveBody into each handler directly, matching the standard pattern used by pulse, sandbox, and flamingo handlers. Auth is now visible at the top of every handler. Made-with: Cursor
There was a problem hiding this comment.
Actionable comments posted: 3
♻️ Duplicate comments (1)
lib/content/primitives/createVideoHandler.ts (1)
34-45:⚠️ Potential issue | 🟠 MajorEnforce
song_urlwhenlipsyncis enabled.When
lipsyncistrueandsong_urlis absent, the handler silently falls back to image-to-video. That’s a behavior mismatch and should return400explicitly.Proposed fix
try { let videoUrl: string | undefined; + if (validated.lipsync && !validated.song_url) { + return NextResponse.json( + { status: "error", error: "song_url is required when lipsync is enabled" }, + { status: 400, headers: getCorsHeaders() }, + ); + } + if (validated.lipsync && validated.song_url) {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/content/primitives/createVideoHandler.ts` around lines 34 - 45, The handler currently falls back to image-to-video when lipsync is true but song_url is missing; update the logic in createVideoHandler so that when validated.lipsync is true and validated.song_url is falsy you immediately return a 400 response (e.g., res.status(400).json(...)) with a clear error message instead of proceeding to the image-to-video branch; locate the conditional around validated.lipsync and validated.song_url (the if block that calls fal.subscribe("fal-ai/ltx-2-19b/audio-to-video"...) and add the explicit validation+early return before calling fal.subscribe so audio-to-video is only attempted when song_url is provided.
🧹 Nitpick comments (2)
lib/content/primitives/createVideoHandler.ts (1)
31-53: Refactor duplicatefal.subscribebranches into one flow.The two branches duplicate request/response handling and push this function past the 50-line guideline. Consolidating model/input selection will improve readability and keep this handler lean.
Refactor sketch
try { - let videoUrl: string | undefined; - - if (validated.lipsync && validated.song_url) { - const result = await fal.subscribe("fal-ai/ltx-2-19b/audio-to-video" as string, { - input: { - image_url: validated.image_url, - audio_url: validated.song_url, - prompt: validated.motion_prompt ?? "person staring at camera, subtle movement", - }, - }); - const resultData = result.data as Record<string, unknown>; - videoUrl = (resultData?.video as Record<string, unknown>)?.url as string | undefined; - } else { - const result = await fal.subscribe("fal-ai/veo3.1/fast/image-to-video" as string, { - input: { - image_url: validated.image_url, - prompt: validated.motion_prompt ?? "nearly still, only natural breathing", - }, - }); - const resultData = result.data as Record<string, unknown>; - videoUrl = (resultData?.video as Record<string, unknown>)?.url as string | undefined; - } + const isLipsync = Boolean(validated.lipsync && validated.song_url); + const model = isLipsync + ? "fal-ai/ltx-2-19b/audio-to-video" + : "fal-ai/veo3.1/fast/image-to-video"; + const input = isLipsync + ? { + image_url: validated.image_url, + audio_url: validated.song_url, + prompt: validated.motion_prompt ?? "person staring at camera, subtle movement", + } + : { + image_url: validated.image_url, + prompt: validated.motion_prompt ?? "nearly still, only natural breathing", + }; + + const result = await fal.subscribe(model, { input }); + const resultData = result.data as Record<string, unknown>; + const videoUrl = (resultData?.video as Record<string, unknown>)?.url as string | undefined;As per coding guidelines: "
lib/**/*.ts: Keep functions under 50 lines" and "**/*.{ts,tsx}: Extract shared logic into reusable utilities following Don't Repeat Yourself (DRY) principle."🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/content/primitives/createVideoHandler.ts` around lines 31 - 53, The two fal.subscribe branches duplicate request/response handling; refactor createVideoHandler by extracting model and input selection into local variables (use validated.lipsync, validated.song_url to choose model string and include audio_url only when present), then call fal.subscribe once and parse resultData to set videoUrl from (result.data?.video)?.url; update references to validated.motion_prompt and validated.image_url as before and keep variable names like videoUrl, resultData, and fal.subscribe to locate the changes.lib/content/primitives/createAudioHandler.ts (1)
15-78: RefactorcreateAudioHandlerinto smaller helpers for maintainability.The handler is doing auth, validation, provider setup, remote call, transformation, and response shaping in one function. Extracting helpers (e.g.,
pickSongUrl,mapWhisperSegments,transcribeAudio) will reduce complexity and improve testability.As per coding guidelines: "
lib/**/*.ts: ... Keep functions under 50 lines".🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/content/primitives/createAudioHandler.ts` around lines 15 - 78, createAudioHandler is too large—split it into small helpers: implement pickSongUrl(validated) to find the first http song URL (used where songUrl is determined), transcribeAudio(songUrl, falKey) to encapsulate fal.config and the fal.subscribe call and return the raw result, and mapWhisperSegments(whisperData) to transform result.data.chunks into the {start,end,text} segments and extract fullLyrics; then refactor createAudioHandler to orchestrate: call validateAuthContext/validatePrimitiveBody as before, call pickSongUrl, call transcribeAudio and pass its result into mapWhisperSegments, and finally return the shaped NextResponse. Ensure each new helper is small and testable and keep createAudioHandler under 50 lines.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@lib/content/primitives/createAudioHandler.ts`:
- Around line 32-33: The current songUrl extraction (variable songUrl in
createAudioHandler) uses startsWith("http") which is too permissive; update the
validation to trim the candidate string, ensure it begins with either "http://"
or "https://", and verify it is a well-formed URL (e.g., by attempting to
construct a URL object) before using it upstream; if validation fails, set
songUrl to undefined (or return a 400 response) so malformed values never reach
the downstream processing in createAudioHandler.
In `@lib/content/primitives/createImageHandler.ts`:
- Around line 39-45: The code reads provider payload into
resultData/images/image and derives imageUrl but never verifies it's a string;
update the runtime check around imageUrl (used before returning in the
NextResponse) to ensure typeof imageUrl === 'string' (or otherwise
validate/normalize it) and only return when it's a valid string; if it is
missing or not a string, return the same error/400 response path instead of a
truthy non-string value. Ensure you modify the checks that reference resultData,
images, image, and imageUrl so the API contract always returns a string URL.
- Around line 16-21: The code calls validateAuthContext but discards its result
and then accepts artist_account_id from the request body, allowing cross-account
requests; retrieve the authenticated account info from authResult (from
validateAuthContext) and enforce authorization by either removing
artist_account_id from the incoming contract or overriding/validating
validated.artist_account_id against the authenticated account id (e.g., compare
authResult.accountId or authResult.account.id to validated.artist_account_id and
return a 403/NextResponse if they differ); update createImageHandler to bind the
artist to the authenticated account (or reject when they don't match) so
artist_account_id cannot be spoofed.
---
Duplicate comments:
In `@lib/content/primitives/createVideoHandler.ts`:
- Around line 34-45: The handler currently falls back to image-to-video when
lipsync is true but song_url is missing; update the logic in createVideoHandler
so that when validated.lipsync is true and validated.song_url is falsy you
immediately return a 400 response (e.g., res.status(400).json(...)) with a clear
error message instead of proceeding to the image-to-video branch; locate the
conditional around validated.lipsync and validated.song_url (the if block that
calls fal.subscribe("fal-ai/ltx-2-19b/audio-to-video"...) and add the explicit
validation+early return before calling fal.subscribe so audio-to-video is only
attempted when song_url is provided.
---
Nitpick comments:
In `@lib/content/primitives/createAudioHandler.ts`:
- Around line 15-78: createAudioHandler is too large—split it into small
helpers: implement pickSongUrl(validated) to find the first http song URL (used
where songUrl is determined), transcribeAudio(songUrl, falKey) to encapsulate
fal.config and the fal.subscribe call and return the raw result, and
mapWhisperSegments(whisperData) to transform result.data.chunks into the
{start,end,text} segments and extract fullLyrics; then refactor
createAudioHandler to orchestrate: call
validateAuthContext/validatePrimitiveBody as before, call pickSongUrl, call
transcribeAudio and pass its result into mapWhisperSegments, and finally return
the shaped NextResponse. Ensure each new helper is small and testable and keep
createAudioHandler under 50 lines.
In `@lib/content/primitives/createVideoHandler.ts`:
- Around line 31-53: The two fal.subscribe branches duplicate request/response
handling; refactor createVideoHandler by extracting model and input selection
into local variables (use validated.lipsync, validated.song_url to choose model
string and include audio_url only when present), then call fal.subscribe once
and parse resultData to set videoUrl from (result.data?.video)?.url; update
references to validated.motion_prompt and validated.image_url as before and keep
variable names like videoUrl, resultData, and fal.subscribe to locate the
changes.
🪄 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: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 3c31caac-fb16-40f6-b80c-c4cccc389a8a
⛔ Files ignored due to path filters (1)
lib/content/primitives/__tests__/validatePrimitiveBody.test.tsis excluded by!**/*.test.*,!**/__tests__/**and included bylib/**
📒 Files selected for processing (7)
lib/content/primitives/createAudioHandler.tslib/content/primitives/createImageHandler.tslib/content/primitives/createRenderHandler.tslib/content/primitives/createTextHandler.tslib/content/primitives/createUpscaleHandler.tslib/content/primitives/createVideoHandler.tslib/content/primitives/validatePrimitiveBody.ts
🚧 Files skipped from review as they are similar to previous changes (4)
- lib/content/primitives/validatePrimitiveBody.ts
- lib/content/primitives/createRenderHandler.ts
- lib/content/primitives/createUpscaleHandler.ts
- lib/content/primitives/createTextHandler.ts
| const songUrl = validated.songs?.find((s: string) => s.startsWith("http")); | ||
|
|
There was a problem hiding this comment.
Harden songUrl validation to avoid malformed URLs reaching upstream.
startsWith("http") is too permissive ("httpx://...", whitespace-prefixed values, etc.). Invalid values can slip through and fail later as a 500 instead of a clean 400.
🔧 Suggested fix
- const songUrl = validated.songs?.find((s: string) => s.startsWith("http"));
+ const songUrl = validated.songs?.find((s: string) => {
+ try {
+ const url = new URL(s);
+ return url.protocol === "http:" || url.protocol === "https:";
+ } catch {
+ return false;
+ }
+ });📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const songUrl = validated.songs?.find((s: string) => s.startsWith("http")); | |
| const songUrl = validated.songs?.find((s: string) => { | |
| try { | |
| const url = new URL(s); | |
| return url.protocol === "http:" || url.protocol === "https:"; | |
| } catch { | |
| return false; | |
| } | |
| }); |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@lib/content/primitives/createAudioHandler.ts` around lines 32 - 33, The
current songUrl extraction (variable songUrl in createAudioHandler) uses
startsWith("http") which is too permissive; update the validation to trim the
candidate string, ensure it begins with either "http://" or "https://", and
verify it is a well-formed URL (e.g., by attempting to construct a URL object)
before using it upstream; if validation fails, set songUrl to undefined (or
return a 400 response) so malformed values never reach the downstream processing
in createAudioHandler.
| const authResult = await validateAuthContext(request); | ||
| if (authResult instanceof NextResponse) return authResult; | ||
|
|
||
| const validated = await validatePrimitiveBody(request, createImageBodySchema); | ||
| if (validated instanceof NextResponse) return validated; | ||
|
|
There was a problem hiding this comment.
Enforce account-level authorization for artist_account_id.
On Line 16, auth is validated, but the returned account context is discarded. On Line 19, artist_account_id is accepted from input and never bound to the authenticated account. Add an explicit authorization check (or remove this field from the contract if intentionally unused) to prevent cross-account requests from being accepted.
Suggested fix
export async function createImageHandler(request: NextRequest): Promise<NextResponse> {
const authResult = await validateAuthContext(request);
if (authResult instanceof NextResponse) return authResult;
const validated = await validatePrimitiveBody(request, createImageBodySchema);
if (validated instanceof NextResponse) return validated;
+
+ if (validated.artist_account_id !== authResult.accountId) {
+ return NextResponse.json(
+ { status: "error", error: "artist_account_id does not match authenticated account" },
+ { status: 403, headers: getCorsHeaders() },
+ );
+ }📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const authResult = await validateAuthContext(request); | |
| if (authResult instanceof NextResponse) return authResult; | |
| const validated = await validatePrimitiveBody(request, createImageBodySchema); | |
| if (validated instanceof NextResponse) return validated; | |
| const authResult = await validateAuthContext(request); | |
| if (authResult instanceof NextResponse) return authResult; | |
| const validated = await validatePrimitiveBody(request, createImageBodySchema); | |
| if (validated instanceof NextResponse) return validated; | |
| if (validated.artist_account_id !== authResult.accountId) { | |
| return NextResponse.json( | |
| { status: "error", error: "artist_account_id does not match authenticated account" }, | |
| { status: 403, headers: getCorsHeaders() }, | |
| ); | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@lib/content/primitives/createImageHandler.ts` around lines 16 - 21, The code
calls validateAuthContext but discards its result and then accepts
artist_account_id from the request body, allowing cross-account requests;
retrieve the authenticated account info from authResult (from
validateAuthContext) and enforce authorization by either removing
artist_account_id from the incoming contract or overriding/validating
validated.artist_account_id against the authenticated account id (e.g., compare
authResult.accountId or authResult.account.id to validated.artist_account_id and
return a 403/NextResponse if they differ); update createImageHandler to bind the
artist to the authenticated account (or reject when they don't match) so
artist_account_id cannot be spoofed.
New content primitive that accepts a video URL and prompt, analyzes the video content, and returns generated text. Follows the standard handler pattern with validateAuthContext at the top. Made-with: Cursor
Analysis is a separate action, not content creation. Made-with: Cursor
There was a problem hiding this comment.
🧹 Nitpick comments (2)
lib/content/primitives/createAnalyzeHandler.ts (2)
32-45: Consider adding a timeout for the external API call.The
fetchcall to Twelve Labs has no timeout configured. Video analysis can be a long-running operation, and without a timeout, this request could hang indefinitely, potentially exhausting server resources.♻️ Proposed fix using AbortSignal.timeout
+ const ANALYZE_TIMEOUT_MS = 120_000; // 2 minutes for video analysis + try { const response = await fetch(TWELVELABS_ANALYZE_URL, { method: "POST", headers: { "x-api-key": apiKey, "Content-Type": "application/json", }, body: JSON.stringify({ video: { type: "url", url: validated.video_url }, prompt: validated.prompt, temperature: validated.temperature, stream: false, ...(validated.max_tokens && { max_tokens: validated.max_tokens }), }), + signal: AbortSignal.timeout(ANALYZE_TIMEOUT_MS), });Then handle the timeout error in the catch block:
} catch (error) { + if (error instanceof DOMException && error.name === "TimeoutError") { + return NextResponse.json( + { status: "error", error: "Video analysis timed out" }, + { status: 504, headers: getCorsHeaders() }, + ); + } console.error("Video analysis error:", error);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/content/primitives/createAnalyzeHandler.ts` around lines 32 - 45, The fetch to TWELVELABS_ANALYZE_URL (inside createAnalyzeHandler where response is awaited) lacks a timeout and can hang; wrap the request with an AbortController (or AbortSignal.timeout) and pass its signal into fetch, choose a sensible timeout (e.g., configurable constant) and ensure the controller is cleaned up; update the catch that handles the fetch to detect and handle AbortError/timeout separately (log/return a clear timeout error) so long-running video analysis cannot indefinitely block the server.
38-44: Thestreamfield from validated input is ignored.The schema (
createAnalyzeBodySchema) defines astreamfield that defaults tofalse, but the handler hardcodesstream: falseon line 42 instead of usingvalidated.stream. If streaming is intentionally unsupported, consider removing the field from the schema to avoid misleading API consumers.♻️ Option A: Use the validated stream value
body: JSON.stringify({ video: { type: "url", url: validated.video_url }, prompt: validated.prompt, temperature: validated.temperature, - stream: false, + stream: validated.stream, ...(validated.max_tokens && { max_tokens: validated.max_tokens }), }),♻️ Option B: Remove stream from schema if unsupported
In
lib/content/primitives/schemas.ts:export const createAnalyzeBodySchema = z.object({ video_url: z.string().url(), prompt: z.string().min(1).max(2000), temperature: z.number().min(0).max(1).optional().default(0.2), - stream: z.boolean().optional().default(false), max_tokens: z.number().int().min(1).max(4096).optional(), });🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/content/primitives/createAnalyzeHandler.ts` around lines 38 - 44, The handler is hardcoding stream: false instead of using the parsed value from createAnalyzeBodySchema; update the request body construction in createAnalyzeHandler (the place that builds the JSON.stringify payload with video/prompt/temperature) to use validated.stream rather than a constant false, or if streaming truly isn't supported remove the stream field from createAnalyzeBodySchema and any code that expects it (ensure consistency between createAnalyzeHandler and createAnalyzeBodySchema so the validated.stream value is either used or no longer present).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@lib/content/primitives/createAnalyzeHandler.ts`:
- Around line 32-45: The fetch to TWELVELABS_ANALYZE_URL (inside
createAnalyzeHandler where response is awaited) lacks a timeout and can hang;
wrap the request with an AbortController (or AbortSignal.timeout) and pass its
signal into fetch, choose a sensible timeout (e.g., configurable constant) and
ensure the controller is cleaned up; update the catch that handles the fetch to
detect and handle AbortError/timeout separately (log/return a clear timeout
error) so long-running video analysis cannot indefinitely block the server.
- Around line 38-44: The handler is hardcoding stream: false instead of using
the parsed value from createAnalyzeBodySchema; update the request body
construction in createAnalyzeHandler (the place that builds the JSON.stringify
payload with video/prompt/temperature) to use validated.stream rather than a
constant false, or if streaming truly isn't supported remove the stream field
from createAnalyzeBodySchema and any code that expects it (ensure consistency
between createAnalyzeHandler and createAnalyzeBodySchema so the validated.stream
value is either used or no longer present).
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: ce9f8795-1286-4eb2-95d3-bd4755000619
⛔ Files ignored due to path filters (2)
lib/content/primitives/__tests__/createAnalyzeHandler.test.tsis excluded by!**/*.test.*,!**/__tests__/**and included bylib/**lib/content/primitives/__tests__/schemas.test.tsis excluded by!**/*.test.*,!**/__tests__/**and included bylib/**
📒 Files selected for processing (3)
app/api/content/analyze/route.tslib/content/primitives/createAnalyzeHandler.tslib/content/primitives/schemas.ts
🚧 Files skipped from review as they are similar to previous changes (1)
- lib/content/primitives/schemas.ts
content/create/image → content/generate-image content/create/video → content/generate-video content/create/text → content/generate-caption content/create/audio → content/transcribe-audio content/create/render → content/render content/create/upscale → content/upscale content/analyze → content/analyze-video Each route name now honestly describes what it does. Follows cli-for-agents convention of consistent verb-based naming. Made-with: Cursor
- Rename music-specific params: face_guide_url → reference_image_url, song_url → audio_url, songs → audio_urls, song → topic - Remove unused required fields: artist_account_id, template, lipsync from primitive schemas (kept in pipeline schema) - Add optional model param to generate-image, generate-video, transcribe-audio for caller-specified fal model IDs - Replace content/render with content/edit — accepts operations array (trim, crop, resize, overlay_text, mux_audio) or template name for deterministic edit config Made-with: Cursor
- Split createTextHandler into composeCaptionPrompt, callRecoupGenerate, normalizeGeneratedText helpers (SRP) - Create createPrimitiveRoute factory for shared OPTIONS + POST wiring across all 7 content primitive routes (DRY) - Add JSDoc to all new helpers and factory - Run pnpm format on touched files Made-with: Cursor
Video generation shouldn't assume image-to-video is the only mode. Now accepts optional prompt, optional image_url, optional audio_url — the model determines what's needed. Made-with: Cursor
Made-with: Cursor
- Prompt only → veo3.1/text-to-video (standard quality) - Image + prompt → veo3.1/image-to-video (standard quality) - Lipsync + audio → ltx-2-19b/audio-to-video - Removed hardcoded music-specific motion prompt defaults Made-with: Cursor
…, audio Schema additions: - video_url (extend an existing video) - aspect_ratio (auto, 16:9, 9:16) - duration (4s, 6s, 7s, 8s — default 8s) - resolution (720p, 1080p, 4k — default 720p) - negative_prompt - generate_audio (default true) Auto-selects model based on inputs: - Prompt only → veo3.1/text-to-video - Image → veo3.1/image-to-video - Video → veo3.1/extend-video - Lipsync → ltx-2-19b/audio-to-video Server-side defaults: safety_tolerance 6, auto_fix true Made-with: Cursor
…dio default Transcribe audio: - language (default "en", was hardcoded) - chunk_level (word/segment/none, default "word") - diarize (default false — identify different speakers) Upscale: - upscale_factor (1-4, default 2) - target_resolution (720p/1080p/1440p/2160p — overrides factor) Generate video: - generate_audio default changed to false (was true) Made-with: Cursor
Modes: prompt, animate, reference, extend, first-last, lipsync Each maps to a specific Veo 3.1 / LTX model variant. Auto-inferred from inputs when omitted. New params: mode, end_image_url Removed: lipsync boolean (replaced by mode: "lipsync") Added first-last-frame and reference-to-video support Made-with: Cursor
- reference mode: image_url → image_urls (array, like nano-banana-2/edit) - first-last mode: image_url → first_frame_url, end_image_url → last_frame_url - Both verified working with live fal calls Made-with: Cursor
generate-image → image, generate-video → video, generate-caption → caption, transcribe-audio → transcribe, analyze-video → analyze. Edit merged into video as PATCH. Made-with: Cursor
Made-with: Cursor
Made-with: Cursor
…en empty) Made-with: Cursor
…call Removes the fetch to /api/chat/generate (API calling itself over HTTP). Now uses generateText from lib/ai/generateText with LIGHTWEIGHT_MODEL. Eliminates: network round trip, RECOUP_API_KEY dependency for captions, 30s timeout, and the env var debugging headache. Made-with: Cursor
DRY: FAL_KEY check + fal.config() was duplicated in 4 handlers. Extracted to configureFal() — single shared helper. SRP: Video handler was doing mode inference, field mapping, and fal call in one function. Extracted buildFalInput() to handle mode-specific field name mapping (reference→image_urls, first-last→first_frame_url/last_frame_url, etc.). Made-with: Cursor
- Removed stream field from createAnalyzeBodySchema (was always hardcoded to false in handler — misleading) - Extracted primitiveOptionsHandler from createPrimitiveRoute for reuse in video route (which needs both POST and PATCH) Made-with: Cursor
Templates are static JSON configs that each primitive applies server-side when template param is passed: - generate-image: uses template prompt, picks random reference image, appends style rules - generate-caption: injects template caption guide + examples into LLM system prompt - generate-video: picks random mood + movement from template for motion prompt - edit (PATCH video): loads template edit operations as defaults 4 templates shipped: artist-caption-bedroom, artist-caption-outside, artist-caption-stage, album-record-store. Reference images uploaded to Supabase storage with signed URLs. GET /api/content/templates now returns id + description (like skills). Override priority: caller params > template defaults. Made-with: Cursor
…ools - Move PATCH edit handler from /api/content/video to /api/content - Add GET /api/content/templates/[id] detail endpoint - Add template field to video body schema - Make pipeline template optional (remove default) - Create 9 content MCP tools via fetch-proxy DRY pattern (generate_image, generate_video, generate_caption, transcribe_audio, edit_content, upscale_content, analyze_video, list_templates, create_content) - All 1749 tests pass Made-with: Cursor
Summary
Full restructure of content primitive endpoints to be generic, consistent, and well-factored.
Route restructure (verb-qualifier naming)
Generic primitives
Edit endpoint (replaces render)
Auth pattern fix
Code quality (CodeRabbit review)