feat: Agent Signup & Verify endpoints#396
Conversation
Add POST /api/agents/signup and POST /api/agents/verify for programmatic agent onboarding without auth. Agent+ prefix emails get instant API keys; all others require email verification via 6-digit code stored in Privy custom_metadata. New files: - lib/agents/ — validators, handlers, email sender, prefix check - lib/privy/ — createPrivyUser, getPrivyUserByEmail, setPrivyCustomMetadata - app/api/agents/signup/route.ts and verify/route.ts - 5 test files (23 tests passing) Co-Authored-By: Paperclip <noreply@paperclip.ing>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
📝 WalkthroughWalkthroughA new agent onboarding system is implemented, introducing POST endpoints for signup and email verification. The flow includes request validation, Privy user account provisioning, verification code delivery via email, and API key generation for authenticated agents. Changes
Sequence DiagramssequenceDiagram
actor Client
participant Server as Server<br/>(signup route)
participant DB as Database
participant Privy as Privy API
participant Email as Email Service
Client->>Server: POST /api/agents/signup<br/>(email)
Server->>Server: Validate email format
Server->>DB: selectAccountByEmail(email)
alt Account Exists
Server->>Privy: getPrivyUserByEmail(email)
alt Privy User Exists
Privy-->>Server: user {id}
else No Privy User
Server->>Privy: createPrivyUser(email)
Privy-->>Server: {id}
end
Server->>Server: Generate verification code
Server->>Privy: setPrivyCustomMetadata(userId,<br/>code_hash, expiry, attempts)
Privy-->>Server: metadata updated
Server->>Email: sendVerificationEmail(email, code)
Email-->>Server: sent
Server-->>Client: {account_id, api_key: null}
else New Email, agent+ prefix
Server->>DB: createAccount(email)
DB-->>Server: account {id}
Server->>Privy: createPrivyUser(email)
Privy-->>Server: user {id}
Server->>Server: generateApiKey("recoup_sk")
Server->>Server: Hash API key
Server->>DB: insertApiKey(account_id, hashed_key)
DB-->>Server: key stored
Server-->>Client: {account_id, api_key}
else New Email, standard
Server->>DB: createAccount(email)
DB-->>Server: account {id}
Server->>Privy: createPrivyUser(email)
Privy-->>Server: user {id}
Server->>Server: Generate verification code
Server->>Privy: setPrivyCustomMetadata(userId, code_hash, expiry, attempts)
Privy-->>Server: metadata updated
Server->>Email: sendVerificationEmail(email, code)
Email-->>Server: sent
Server-->>Client: {account_id, api_key: null}
end
sequenceDiagram
actor Client
participant Server as Server<br/>(verify route)
participant Privy as Privy API
participant DB as Database
Client->>Server: POST /api/agents/verify<br/>(email, code)
Server->>Server: Validate email & code format
Server->>Privy: getPrivyUserByEmail(email)
Privy-->>Server: user {id, custom_metadata}
Server->>Server: Extract code_hash, expires_at,<br/>attempts from metadata
alt Metadata Missing or Expired
Server-->>Client: 400 Error
else Attempts ≥ 5
Server-->>Client: 429 Rate Limited
else Code Mismatch
Server->>Privy: setPrivyCustomMetadata(userId,<br/>increment attempts)
Privy-->>Server: metadata updated
Server-->>Client: 400 Error
else Code Matches
Server->>DB: selectAccountByEmail(email)
DB-->>Server: account {id}
Server->>Server: generateApiKey("recoup_sk")
Server->>Server: Hash API key
Server->>DB: insertApiKey(account_id, hashed_key)
DB-->>Server: key stored
Server->>Privy: setPrivyCustomMetadata(userId,<br/>clear metadata)
Privy-->>Server: metadata cleared
Server-->>Client: {account_id, api_key,<br/>message: "Verified"}
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes 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 docstrings
🧪 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: 3
🧹 Nitpick comments (5)
app/api/agents/verify/route.ts (1)
27-27: Redundant type assertion.The
as AgentVerifyBodycast is unnecessary. Sincevalidatedis not aNextResponseat this point (checked on line 23), TypeScript already narrows the type toAgentVerifyBody.♻️ Cleaner version
- return agentVerifyHandler(validated as AgentVerifyBody); + return agentVerifyHandler(validated);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/api/agents/verify/route.ts` at line 27, The return statement uses an unnecessary type assertion "as AgentVerifyBody"; remove the redundant cast and directly pass validated to agentVerifyHandler since validated has already been narrowed away from NextResponse (see validated and agentVerifyHandler references) — update the return to simply return agentVerifyHandler(validated) so TypeScript infers the correct AgentVerifyBody type without the explicit cast.lib/agents/sendVerificationEmail.ts (1)
18-20: UseNextResponsefor consistency with codebase patterns.The codebase convention (see
lib/emails/processAndSendEmail.ts) usesinstanceof NextResponserather thaninstanceof Response. While both work sinceNextResponseextendsResponse, using the more specific type improves consistency and clarity.♻️ Suggested fix
+import { NextResponse } from "next/server"; import { sendEmailWithResend } from "@/lib/emails/sendEmail"; import { RECOUP_FROM_EMAIL } from "@/lib/const";- if (result instanceof Response) { + if (result instanceof NextResponse) { throw new Error("Failed to send verification email"); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/agents/sendVerificationEmail.ts` around lines 18 - 20, Change the type-check to use NextResponse instead of Response for consistency: replace the "result instanceof Response" check in sendVerificationEmail (the block that throws "Failed to send verification email") with "result instanceof NextResponse" and add the appropriate NextResponse import (e.g., from 'next/server' or matching project imports) if it's not already imported.lib/agents/validateAgentVerifyBody.ts (1)
5-8: Consider stricter validation for the verification code format.The PR description mentions a "6-digit verification code," but the schema only validates that
codeis a non-empty string. Adding format validation here would reject malformed codes earlier and provide clearer error messages.♻️ Suggested stricter validation
export const agentVerifyBodySchema = z.object({ email: z.string({ message: "email is required" }).email("email must be a valid email address"), - code: z.string({ message: "code is required" }).min(1, "code cannot be empty"), + code: z.string({ message: "code is required" }).regex(/^\d{6}$/, "code must be a 6-digit number"), });🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/agents/validateAgentVerifyBody.ts` around lines 5 - 8, The agentVerifyBodySchema currently only checks that code is a non-empty string; update agentVerifyBodySchema to enforce the expected 6-digit numeric format by replacing the loose validation on the code field with a stricter rule (e.g., a length or regex-based constraint that ensures exactly six digits) and provide a clear error message like "code must be a 6-digit numeric code" so malformed codes are rejected during schema validation.lib/privy/getPrivyUserByEmail.ts (1)
10-18: Consider adding a request timeout for resilience.External HTTP calls without timeouts can hang indefinitely if the Privy API becomes unresponsive, potentially blocking resources. Consider using
AbortControllerwith a timeout.♻️ Example with timeout
export async function getPrivyUserByEmail( email: string, ): Promise<{ id: string; custom_metadata?: Record<string, unknown> } | null> { + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), 10000); + - const response = await fetch("https://api.privy.io/v1/users/email/address", { + const response = await fetch("https://api.privy.io/v1/users/email/address", { method: "POST", headers: { "Content-Type": "application/json", "privy-app-id": process.env.PRIVY_APP_ID!, Authorization: `Basic ${btoa(process.env.PRIVY_APP_ID! + ":" + process.env.PRIVY_PROJECT_SECRET!)}`, }, body: JSON.stringify({ email }), + signal: controller.signal, - }); + }).finally(() => clearTimeout(timeoutId));🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/privy/getPrivyUserByEmail.ts` around lines 10 - 18, Add an AbortController-based request timeout around the fetch call in getPrivyUserByEmail: create an AbortController, pass controller.signal to fetch, start a timer (e.g., setTimeout) that calls controller.abort() after a configurable timeout (e.g., 5s), and clear the timer once the response is received; handle the abort/timeout case (catch AbortError or check signal.aborted) and throw or return a clear timeout error so callers can handle it. Ensure the Authorization/header logic and body remain unchanged and the controller is cleaned up to avoid leaks.lib/agents/agentSignupHandler.ts (1)
61-67: Sequential operations without transaction handling.
createAccountWithEmailperforms four sequential database/service operations. If any operation fails midway (e.g.,insertCreditsUsagefails afterinsertAccountEmailsucceeds), you'll have orphaned partial state.For a signup flow, this could leave accounts in an inconsistent state. Consider:
- Using database transactions if Supabase supports them for these operations
- Adding compensating cleanup in the catch block
- Making the flow idempotent so retries are safe
This isn't a blocker for this PR, but worth tracking for reliability improvements.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/agents/agentSignupHandler.ts` around lines 61 - 67, The createAccountWithEmail function performs multiple DB/service calls (insertAccount, insertAccountEmail, insertCreditsUsage, assignAccountToOrg) without transaction or rollback, so if a step fails you can leave partial state; update createAccountWithEmail to run these operations in a single DB transaction (or a transactional wrapper) and/or add a try/catch that performs compensating cleanup (delete account, delete email, revoke credits, remove org assignment) on error, and ensure the operations are idempotent (safe to retry) by checking for existing records before re-creating them; reference the createAccountWithEmail function and the called helpers (insertAccount, insertAccountEmail, insertCreditsUsage, assignAccountToOrg) when implementing the transaction/cleanup logic.
🤖 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/agents/agentSignupHandler.ts`:
- Around line 75-81: The function generateAndStoreApiKey uses a non-null
assertion on process.env.PROJECT_SECRET which can throw silently if the env var
is missing; replace the assertion with a guarded lookup (e.g., const
projectSecret = process.env.PROJECT_SECRET) and if it's falsy throw a clear
Error("PROJECT_SECRET environment variable is required") before calling
hashApiKey, and apply the same pattern to the other occurrence referenced (the
usage at line ~111) so both hashApiKey calls validate the secret at runtime
instead of using the ! operator.
In `@lib/agents/agentVerifyHandler.ts`:
- Line 95: The call to insertApiKey is unchecked and may return an error while
you still return the raw API key to the client; change the flow around
insertApiKey in agentVerifyHandler (the call with keyHash and
insertApiKey({...})) so you destructure its result ({ data, error }), verify
error is null and data exists before returning the plain key, and on failure
abort (throw or return a proper error response) so you never expose a key that
wasn't persisted; ensure you reference insertApiKey and the local key/keyHash
variables when implementing the guard.
- Around line 47-56: The expiry check in agentVerifyHandler.ts currently skips
validation when metadata.verification_expires_at is missing, allowing codes to
be accepted indefinitely; update the verify flow in the handler that reads
metadata (use the same metadata object and the existing verification_expires_at
symbol) to require presence of metadata.verification_expires_at and return the
same NextResponse.json({ error: GENERIC_ERROR }, { status: 400, headers:
getCorsHeaders() }) if it is absent or unparsable, or alternatively set a strict
default expiry and treat missing/unparsable values as expired—ensure the code
paths around the existing Date parsing/compare use this enforced behavior so
missing fields cannot bypass expiry checks.
---
Nitpick comments:
In `@app/api/agents/verify/route.ts`:
- Line 27: The return statement uses an unnecessary type assertion "as
AgentVerifyBody"; remove the redundant cast and directly pass validated to
agentVerifyHandler since validated has already been narrowed away from
NextResponse (see validated and agentVerifyHandler references) — update the
return to simply return agentVerifyHandler(validated) so TypeScript infers the
correct AgentVerifyBody type without the explicit cast.
In `@lib/agents/agentSignupHandler.ts`:
- Around line 61-67: The createAccountWithEmail function performs multiple
DB/service calls (insertAccount, insertAccountEmail, insertCreditsUsage,
assignAccountToOrg) without transaction or rollback, so if a step fails you can
leave partial state; update createAccountWithEmail to run these operations in a
single DB transaction (or a transactional wrapper) and/or add a try/catch that
performs compensating cleanup (delete account, delete email, revoke credits,
remove org assignment) on error, and ensure the operations are idempotent (safe
to retry) by checking for existing records before re-creating them; reference
the createAccountWithEmail function and the called helpers (insertAccount,
insertAccountEmail, insertCreditsUsage, assignAccountToOrg) when implementing
the transaction/cleanup logic.
In `@lib/agents/sendVerificationEmail.ts`:
- Around line 18-20: Change the type-check to use NextResponse instead of
Response for consistency: replace the "result instanceof Response" check in
sendVerificationEmail (the block that throws "Failed to send verification
email") with "result instanceof NextResponse" and add the appropriate
NextResponse import (e.g., from 'next/server' or matching project imports) if
it's not already imported.
In `@lib/agents/validateAgentVerifyBody.ts`:
- Around line 5-8: The agentVerifyBodySchema currently only checks that code is
a non-empty string; update agentVerifyBodySchema to enforce the expected 6-digit
numeric format by replacing the loose validation on the code field with a
stricter rule (e.g., a length or regex-based constraint that ensures exactly six
digits) and provide a clear error message like "code must be a 6-digit numeric
code" so malformed codes are rejected during schema validation.
In `@lib/privy/getPrivyUserByEmail.ts`:
- Around line 10-18: Add an AbortController-based request timeout around the
fetch call in getPrivyUserByEmail: create an AbortController, pass
controller.signal to fetch, start a timer (e.g., setTimeout) that calls
controller.abort() after a configurable timeout (e.g., 5s), and clear the timer
once the response is received; handle the abort/timeout case (catch AbortError
or check signal.aborted) and throw or return a clear timeout error so callers
can handle it. Ensure the Authorization/header logic and body remain unchanged
and the controller is cleaned up to avoid leaks.
🪄 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: a56bb2d5-dcf0-4969-8fdd-3ba9d88c5d12
⛔ Files ignored due to path filters (5)
lib/agents/__tests__/agentSignupHandler.test.tsis excluded by!**/*.test.*,!**/__tests__/**and included bylib/**lib/agents/__tests__/agentVerifyHandler.test.tsis excluded by!**/*.test.*,!**/__tests__/**and included bylib/**lib/agents/__tests__/isAgentPrefixEmail.test.tsis excluded by!**/*.test.*,!**/__tests__/**and included bylib/**lib/agents/__tests__/validateAgentSignupBody.test.tsis excluded by!**/*.test.*,!**/__tests__/**and included bylib/**lib/agents/__tests__/validateAgentVerifyBody.test.tsis excluded by!**/*.test.*,!**/__tests__/**and included bylib/**
📒 Files selected for processing (11)
app/api/agents/signup/route.tsapp/api/agents/verify/route.tslib/agents/agentSignupHandler.tslib/agents/agentVerifyHandler.tslib/agents/isAgentPrefixEmail.tslib/agents/sendVerificationEmail.tslib/agents/validateAgentSignupBody.tslib/agents/validateAgentVerifyBody.tslib/privy/createPrivyUser.tslib/privy/getPrivyUserByEmail.tslib/privy/setPrivyCustomMetadata.ts
| async function generateAndStoreApiKey(accountId: string): Promise<string> { | ||
| const today = new Date().toISOString().slice(0, 10); | ||
| const rawKey = generateApiKey("recoup_sk"); | ||
| const keyHash = hashApiKey(rawKey, process.env.PROJECT_SECRET!); | ||
| await insertApiKey({ name: `Agent ${today}`, account: accountId, key_hash: keyHash }); | ||
| return rawKey; | ||
| } |
There was a problem hiding this comment.
Non-null assertion on PROJECT_SECRET could cause silent failures.
The process.env.PROJECT_SECRET! assertion on line 78 (and similarly on line 111) will throw a runtime error if the environment variable is missing. Consider validating this at application startup or adding a guard:
const projectSecret = process.env.PROJECT_SECRET;
if (!projectSecret) {
throw new Error("PROJECT_SECRET environment variable is required");
}
const keyHash = hashApiKey(rawKey, projectSecret);Alternatively, if this is validated elsewhere at startup, this is acceptable—but explicit checks make the code more defensive.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@lib/agents/agentSignupHandler.ts` around lines 75 - 81, The function
generateAndStoreApiKey uses a non-null assertion on process.env.PROJECT_SECRET
which can throw silently if the env var is missing; replace the assertion with a
guarded lookup (e.g., const projectSecret = process.env.PROJECT_SECRET) and if
it's falsy throw a clear Error("PROJECT_SECRET environment variable is
required") before calling hashApiKey, and apply the same pattern to the other
occurrence referenced (the usage at line ~111) so both hashApiKey calls validate
the secret at runtime instead of using the ! operator.
| // Expired | ||
| if (metadata.verification_expires_at) { | ||
| const expiresAt = new Date(metadata.verification_expires_at).getTime(); | ||
| if (Date.now() > expiresAt) { | ||
| return NextResponse.json( | ||
| { error: GENERIC_ERROR }, | ||
| { status: 400, headers: getCorsHeaders() }, | ||
| ); | ||
| } | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Check how verification metadata is set in agentSignupHandler to ensure expiry is always included
rg -n "verification_code_hash|verification_expires_at" --type=ts -C 3Repository: recoupable/api
Length of output: 5178
Make expiry validation mandatory, not conditional.
The signup handler correctly always sets both verification_code_hash and verification_expires_at together. However, in the verify handler (lines 48-50), the expiry check is conditional—if verification_expires_at is missing, the entire validation is skipped without error. This allows codes to remain valid indefinitely if the metadata is incomplete, manually created, or corrupted. Either require the field to always be present and fail early if missing, or enforce a default expiry behavior to prevent silent security bypasses.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@lib/agents/agentVerifyHandler.ts` around lines 47 - 56, The expiry check in
agentVerifyHandler.ts currently skips validation when
metadata.verification_expires_at is missing, allowing codes to be accepted
indefinitely; update the verify flow in the handler that reads metadata (use the
same metadata object and the existing verification_expires_at symbol) to require
presence of metadata.verification_expires_at and return the same
NextResponse.json({ error: GENERIC_ERROR }, { status: 400, headers:
getCorsHeaders() }) if it is absent or unparsable, or alternatively set a strict
default expiry and treat missing/unparsable values as expired—ensure the code
paths around the existing Date parsing/compare use this enforced behavior so
missing fields cannot bypass expiry checks.
| const today = new Date().toISOString().slice(0, 10); | ||
| const rawKey = generateApiKey("recoup_sk"); | ||
| const keyHash = hashApiKey(rawKey, process.env.PROJECT_SECRET!); | ||
| await insertApiKey({ name: `Agent ${today}`, account: accountId, key_hash: keyHash }); |
There was a problem hiding this comment.
Unchecked error from insertApiKey could return API key that wasn't persisted.
Per the insertApiKey signature (context snippet 2), the function returns { data, error }. If the insert fails, this code proceeds to return the raw API key to the user even though it wasn't stored in the database. The user would receive an invalid key.
🐛 Proposed fix
- await insertApiKey({ name: `Agent ${today}`, account: accountId, key_hash: keyHash });
+ const { error: insertError } = await insertApiKey({ name: `Agent ${today}`, account: accountId, key_hash: keyHash });
+ if (insertError) {
+ console.error("[ERROR] agentVerifyHandler: Failed to insert API key:", insertError);
+ return NextResponse.json(
+ { error: "Internal server error" },
+ { status: 500, 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.
| await insertApiKey({ name: `Agent ${today}`, account: accountId, key_hash: keyHash }); | |
| const { error: insertError } = await insertApiKey({ name: `Agent ${today}`, account: accountId, key_hash: keyHash }); | |
| if (insertError) { | |
| console.error("[ERROR] agentVerifyHandler: Failed to insert API key:", insertError); | |
| return NextResponse.json( | |
| { error: "Internal server error" }, | |
| { status: 500, headers: getCorsHeaders() }, | |
| ); | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@lib/agents/agentVerifyHandler.ts` at line 95, The call to insertApiKey is
unchecked and may return an error while you still return the raw API key to the
client; change the flow around insertApiKey in agentVerifyHandler (the call with
keyHash and insertApiKey({...})) so you destructure its result ({ data, error
}), verify error is null and data exists before returning the plain key, and on
failure abort (throw or return a proper error response) so you never expose a
key that wasn't persisted; ensure you reference insertApiKey and the local
key/keyHash variables when implementing the guard.
There was a problem hiding this comment.
6 issues found across 16 files
Confidence score: 2/5
- High-confidence, user-facing failures are likely in
lib/agents/agentVerifyHandler.ts: theinsertApiKeyresult error is not handled, so users can be given API keys that were never persisted and will fail on authenticated requests. lib/agents/agentVerifyHandler.tsalso has a fail-open verification path: ifverification_expires_atis missing, expiry checks are skipped and codes may remain valid indefinitely.- Given two concrete high-severity logic issues (7-8/10, confidence 9/10), this is above normal merge risk; the remaining findings in
lib/agents/agentSignupHandler.tsandlib/agents/validateAgentVerifyBody.tsare mostly quality/validation improvements but still worth cleaning up. - Pay close attention to
lib/agents/agentVerifyHandler.ts,lib/agents/validateAgentVerifyBody.ts,lib/agents/agentSignupHandler.ts- auth token persistence and verification-expiry correctness are the main risk drivers, with input/code-generation edge cases behind them.
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="lib/agents/agentSignupHandler.ts">
<violation number="1" location="lib/agents/agentSignupHandler.ts:1">
P1: Custom agent: **Enforce Clear Code Style and Maintainability Practices**
At 158 lines this file exceeds the 100-line limit and bundles multiple concerns. Extract `createAccountWithEmail`, `generateAndStoreApiKey`, and `storeVerificationCode` into separate files under `lib/agents/` — each is an independent unit with its own responsibility.</violation>
<violation number="2" location="lib/agents/agentSignupHandler.ts:70">
P2: Custom agent: **Flag AI Slop and Fabricated Changes**
Every private helper has a full JSDoc block that restates the function name and typed parameters (e.g. `@param email - The email address` on a parameter already named `email: string`). These are narrating comments that add no information beyond what the signatures already express. Remove or condense them — a one-line `//` comment is enough where the name isn't self-evident.</violation>
<violation number="3" location="lib/agents/agentSignupHandler.ts:110">
P2: `randomInt` upper bound is exclusive, so `999999` is never generated. Use `randomInt(100000, 1000000)` to cover all 6-digit codes.</violation>
</file>
<file name="lib/agents/validateAgentVerifyBody.ts">
<violation number="1" location="lib/agents/validateAgentVerifyBody.ts:7">
P2: The `code` schema should enforce the expected 6-digit format (e.g., `.regex(/^\d{6}$/)`) so obviously invalid codes are rejected at the validation layer without consuming rate-limited verification attempts.</violation>
</file>
<file name="lib/agents/agentVerifyHandler.ts">
<violation number="1" location="lib/agents/agentVerifyHandler.ts:48">
P1: Missing `verification_expires_at` causes the expiry check to be skipped entirely, making the code valid forever. Treat a missing expiry as expired to fail-safe.</violation>
<violation number="2" location="lib/agents/agentVerifyHandler.ts:95">
P1: `insertApiKey` returns `{ data, error }` but the error is not checked. If the insert fails, the user receives an API key that was never persisted and will not work on any authenticated endpoint.</violation>
</file>
Architecture diagram
sequenceDiagram
participant Client
participant Signup as NEW: /api/agents/signup
participant Verify as NEW: /api/agents/verify
participant Privy as NEW: Privy API
participant DB as Supabase
participant Email as Resend
Note over Client,Email: Agent Registration Flow
Client->>Signup: POST { email }
Signup->>DB: selectAccountByEmail()
alt NEW: Email starts with 'agent+' (Instant Access)
Signup->>DB: createAccountWithEmail()
Signup->>Privy: NEW: createPrivyUser()
Signup->>DB: generateAndStoreApiKey()
Signup-->>Client: 200 OK { api_key: "recoup_sk_..." }
else Standard Email (Requires Verification)
opt Account doesn't exist
Signup->>DB: createAccountWithEmail()
end
Signup->>Privy: NEW: getPrivyUserByEmail() / createPrivyUser()
Signup->>Privy: NEW: setPrivyCustomMetadata() (code hash + expiry)
Signup->>Email: NEW: sendVerificationEmail(code)
Signup-->>Client: 200 OK { api_key: null }
end
Note over Client,Email: Agent Verification Flow (standard email path)
Client->>Verify: POST { email, code }
Verify->>Privy: NEW: getPrivyUserByEmail() (fetch metadata)
alt NEW: Invalid Code / Expired / Max Attempts
Verify->>Privy: NEW: setPrivyCustomMetadata() (increment attempts)
Verify-->>Client: 400/429 Error
else Valid Code
Verify->>DB: selectAccountByEmail()
Verify->>DB: generateAndStoreApiKey()
Verify->>Privy: NEW: setPrivyCustomMetadata() (clear metadata)
Verify-->>Client: 200 OK { api_key: "recoup_sk_..." }
end
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review, or fix all with cubic.
| @@ -0,0 +1,158 @@ | |||
| import { NextResponse } from "next/server"; | |||
There was a problem hiding this comment.
P1: Custom agent: Enforce Clear Code Style and Maintainability Practices
At 158 lines this file exceeds the 100-line limit and bundles multiple concerns. Extract createAccountWithEmail, generateAndStoreApiKey, and storeVerificationCode into separate files under lib/agents/ — each is an independent unit with its own responsibility.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At lib/agents/agentSignupHandler.ts, line 1:
<comment>At 158 lines this file exceeds the 100-line limit and bundles multiple concerns. Extract `createAccountWithEmail`, `generateAndStoreApiKey`, and `storeVerificationCode` into separate files under `lib/agents/` — each is an independent unit with its own responsibility.</comment>
<file context>
@@ -0,0 +1,158 @@
+import { NextResponse } from "next/server";
+import { randomInt } from "crypto";
+import { getCorsHeaders } from "@/lib/networking/getCorsHeaders";
</file context>
| } | ||
|
|
||
| // Expired | ||
| if (metadata.verification_expires_at) { |
There was a problem hiding this comment.
P1: Missing verification_expires_at causes the expiry check to be skipped entirely, making the code valid forever. Treat a missing expiry as expired to fail-safe.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At lib/agents/agentVerifyHandler.ts, line 48:
<comment>Missing `verification_expires_at` causes the expiry check to be skipped entirely, making the code valid forever. Treat a missing expiry as expired to fail-safe.</comment>
<file context>
@@ -0,0 +1,111 @@
+ }
+
+ // Expired
+ if (metadata.verification_expires_at) {
+ const expiresAt = new Date(metadata.verification_expires_at).getTime();
+ if (Date.now() > expiresAt) {
</file context>
| const today = new Date().toISOString().slice(0, 10); | ||
| const rawKey = generateApiKey("recoup_sk"); | ||
| const keyHash = hashApiKey(rawKey, process.env.PROJECT_SECRET!); | ||
| await insertApiKey({ name: `Agent ${today}`, account: accountId, key_hash: keyHash }); |
There was a problem hiding this comment.
P1: insertApiKey returns { data, error } but the error is not checked. If the insert fails, the user receives an API key that was never persisted and will not work on any authenticated endpoint.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At lib/agents/agentVerifyHandler.ts, line 95:
<comment>`insertApiKey` returns `{ data, error }` but the error is not checked. If the insert fails, the user receives an API key that was never persisted and will not work on any authenticated endpoint.</comment>
<file context>
@@ -0,0 +1,111 @@
+ const today = new Date().toISOString().slice(0, 10);
+ const rawKey = generateApiKey("recoup_sk");
+ const keyHash = hashApiKey(rawKey, process.env.PROJECT_SECRET!);
+ await insertApiKey({ name: `Agent ${today}`, account: accountId, key_hash: keyHash });
+
+ // Clear custom_metadata
</file context>
| } | ||
|
|
||
| /** | ||
| * Generates a new API key, hashes it, and stores it in the database. |
There was a problem hiding this comment.
P2: Custom agent: Flag AI Slop and Fabricated Changes
Every private helper has a full JSDoc block that restates the function name and typed parameters (e.g. @param email - The email address on a parameter already named email: string). These are narrating comments that add no information beyond what the signatures already express. Remove or condense them — a one-line // comment is enough where the name isn't self-evident.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At lib/agents/agentSignupHandler.ts, line 70:
<comment>Every private helper has a full JSDoc block that restates the function name and typed parameters (e.g. `@param email - The email address` on a parameter already named `email: string`). These are narrating comments that add no information beyond what the signatures already express. Remove or condense them — a one-line `//` comment is enough where the name isn't self-evident.</comment>
<file context>
@@ -0,0 +1,158 @@
+}
+
+/**
+ * Generates a new API key, hashes it, and stores it in the database.
+ *
+ * @param accountId - The account ID to associate the key with
</file context>
|
|
||
| export const agentVerifyBodySchema = z.object({ | ||
| email: z.string({ message: "email is required" }).email("email must be a valid email address"), | ||
| code: z.string({ message: "code is required" }).min(1, "code cannot be empty"), |
There was a problem hiding this comment.
P2: The code schema should enforce the expected 6-digit format (e.g., .regex(/^\d{6}$/)) so obviously invalid codes are rejected at the validation layer without consuming rate-limited verification attempts.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At lib/agents/validateAgentVerifyBody.ts, line 7:
<comment>The `code` schema should enforce the expected 6-digit format (e.g., `.regex(/^\d{6}$/)`) so obviously invalid codes are rejected at the validation layer without consuming rate-limited verification attempts.</comment>
<file context>
@@ -0,0 +1,37 @@
+
+export const agentVerifyBodySchema = z.object({
+ email: z.string({ message: "email is required" }).email("email must be a valid email address"),
+ code: z.string({ message: "code is required" }).min(1, "code cannot be empty"),
+});
+
</file context>
| * @param email - The email address to send the code to | ||
| */ | ||
| async function storeVerificationCode(email: string): Promise<void> { | ||
| const code = randomInt(100000, 999999).toString(); |
There was a problem hiding this comment.
P2: randomInt upper bound is exclusive, so 999999 is never generated. Use randomInt(100000, 1000000) to cover all 6-digit codes.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At lib/agents/agentSignupHandler.ts, line 110:
<comment>`randomInt` upper bound is exclusive, so `999999` is never generated. Use `randomInt(100000, 1000000)` to cover all 6-digit codes.</comment>
<file context>
@@ -0,0 +1,158 @@
+ * @param email - The email address to send the code to
+ */
+async function storeVerificationCode(email: string): Promise<void> {
+ const code = randomInt(100000, 999999).toString();
+ const codeHash = hashApiKey(code, process.env.PROJECT_SECRET!);
+ const expiresAt = new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString();
</file context>
Summary
POST /api/agents/signup— unauthenticated endpoint for agent registrationagent+prefix emails get instant API key on first signupPOST /api/agents/verify— unauthenticated endpoint for email verificationcreatePrivyUser,getPrivyUserByEmail,setPrivyCustomMetadataTest plan
pnpm test lib/agents/__tests__/)POST /api/agents/signupwithagent+email returnsapi_keyimmediatelyPOST /api/agents/signupwith normal email returnsapi_key: nulland sends verification emailPOST /api/agents/signupwith existing account email sends verification codePOST /api/agents/verifywith correct code returnsapi_keyPOST /api/agents/verifywith wrong code increments attempts and returns 400POST /api/agents/verifyafter 5 failures returns 429POST /api/agents/verifywith expired code returns 400api_keyworks on authenticated endpoints (e.g.GET /api/artists)🤖 Generated with Claude Code
Summary by cubic
Adds unauthenticated agent signup and email verification endpoints to enable programmatic onboarding.
agent+emails get an instant API key; all others verify via a 6‑digit code with consistent responses to avoid email enumeration.agent+gets an immediate API key; otherwise stores a 6‑digit code in Privy and sends it via Resend. Uniform response shape and CORS enabled.createPrivyUser,getPrivyUserByEmail,setPrivyCustomMetadata; Zod validators for request bodies; route handlers with tests.Written for commit 0ac00a5. Summary will update on new commits.
Summary by CodeRabbit
Release Notes