Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -261,12 +261,12 @@ const { accountId, orgId, authToken } = authResult;

`validateAuthContext` handles:
- Both `x-api-key` and `Authorization: Bearer` authentication
- Account ID override validation (org keys can access member accounts)
- Account ID override validation (accounts with shared org membership can access member accounts)
- Organization access validation

### MCP Tools

**CRITICAL: Never manually extract `accountId` from `extra.authInfo` (e.g. `authInfo?.extra?.accountId`).** Always use `resolveAccountId()` — it handles validation, org-key overrides, and access control in one place.
**CRITICAL: Never manually extract `accountId` from `extra.authInfo` (e.g. `authInfo?.extra?.accountId`).** Always use `resolveAccountId()` — it handles validation, org-membership overrides, and access control in one place.

```typescript
import { resolveAccountId } from "@/lib/mcp/resolveAccountId";
Expand Down
1 change: 1 addition & 0 deletions app/api/accounts/[id]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export async function OPTIONS() {
* - id (required): The unique identifier of the account (UUID)
*
* @param request - The request object
* @param params.params
* @param params - Route params containing the account ID
* @returns A NextResponse with account data
*/
Expand Down
5 changes: 5 additions & 0 deletions app/api/admins/privy/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,16 @@ import { getPrivyLoginsHandler } from "@/lib/admins/privy/getPrivyLoginsHandler"
* Returns Privy login statistics for the requested time period.
* Supports daily (last 24h), weekly (last 7 days), and monthly (last 30 days) periods.
* Requires admin authentication.
*
* @param request
*/
export async function GET(request: NextRequest): Promise<NextResponse> {
return getPrivyLoginsHandler(request);
}

/**
*
*/
export async function OPTIONS(): Promise<NextResponse> {
return new NextResponse(null, { status: 204, headers: getCorsHeaders() });
}
2 changes: 1 addition & 1 deletion app/api/artists/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ export async function GET(request: NextRequest) {
* Request body:
* - name (required): The name of the artist to create
* - account_id (optional): The ID of the account to create the artist for (UUID).
* Only required for organization API keys creating artists on behalf of other accounts.
* Only required when creating artists on behalf of other accounts within a shared organization.
* - organization_id (optional): The organization ID to link the new artist to (UUID)
*
* @param request - The request object containing JSON body
Expand Down
2 changes: 1 addition & 1 deletion app/api/chat/generate/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export async function OPTIONS() {
* - artistId: Optional UUID of the artist account
* - model: Optional model ID override
* - excludeTools: Optional array of tool names to exclude
* - accountId: Optional accountId override (requires org API key)
* - accountId: Optional accountId override (requires shared org membership or admin access)
*
* Response body:
* - text: The generated text response
Expand Down
2 changes: 1 addition & 1 deletion app/api/chat/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export async function OPTIONS() {
* - artistId: Optional UUID of the artist account
* - model: Optional model ID override
* - excludeTools: Optional array of tool names to exclude
* - accountId: Optional accountId override (requires org API key)
* - accountId: Optional accountId override (requires shared org membership or admin access)
*
* @param request - The request object
* @returns A streaming response or error
Expand Down
2 changes: 2 additions & 0 deletions app/api/coding-agent/[platform]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import "@/lib/coding-agent/handlers/registerHandlers";
* Handles webhook verification handshakes (e.g. WhatsApp hub.challenge).
*
* @param request - The incoming verification request
* @param params.params
* @param params - Route params containing the platform name
*/
export async function GET(
Expand All @@ -34,6 +35,7 @@ export async function GET(
* Handles Slack and WhatsApp webhooks via dynamic [platform] segment.
*
* @param request - The incoming webhook request
* @param params.params
* @param params - Route params containing the platform name
*/
export async function POST(
Expand Down
2 changes: 1 addition & 1 deletion app/api/notifications/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export async function OPTIONS() {
* - cc (optional): array of CC email addresses
* - headers (optional): custom email headers
* - room_id (optional): room ID for chat link in footer
* - account_id (optional): UUID of the account to send to (org keys only)
* - account_id (optional): UUID of the account to send to (requires shared org membership or admin access)
*
* @param request - The request object.
* @returns A NextResponse with send result.
Expand Down
5 changes: 2 additions & 3 deletions app/api/organizations/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,11 @@ export async function OPTIONS() {
* Retrieves all organizations an account belongs to.
* Requires authentication via x-api-key or Authorization bearer token.
*
* For personal keys: returns the key owner's organizations.
* For org keys: returns organizations for all accounts in the org.
* Returns the key owner's organizations.
* For Recoup admin: returns all organizations.
*
* Query parameters:
* - account_id (optional): Filter to a specific account (org keys only)
* - account_id (optional): Filter to a specific account (requires shared org membership or admin access)
*
* @param request - The request object
* @returns A NextResponse with organizations data
Expand Down
7 changes: 3 additions & 4 deletions app/api/pulses/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,11 @@ export async function OPTIONS() {
* Retrieves pulse statuses for accounts.
* Requires authentication via x-api-key header or Authorization bearer token.
*
* For personal keys: Returns array with single pulse for the account.
* For org keys: Returns array of pulses for all accounts in the organization.
* Returns array with pulse for the authenticated account.
* For Recoup admin key: Returns array of ALL pulse records.
*
* Query parameters:
* - account_id: For org API keys, filter to a specific account within the organization
* - account_id: Filter to a specific account (requires shared org membership or admin access)
* - active: Filter by active status (true/false). If undefined, returns all.
*
* @param request - The request object.
Expand All @@ -47,7 +46,7 @@ export async function GET(request: NextRequest) {
*
* Body parameters:
* - active (required): boolean - Whether pulse is active for this account
* - account_id (optional): For org API keys, target a specific account within the organization
* - account_id (optional): Target a specific account (requires shared org membership or admin access)
*
* @param request - The request object containing the body with active boolean.
* @returns A NextResponse with array of pulse statuses.
Expand Down
7 changes: 3 additions & 4 deletions app/api/sandboxes/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,14 +106,13 @@ export async function PATCH(request: NextRequest): Promise<Response> {
* DELETE /api/sandboxes
*
* Deletes the GitHub repository and snapshot record for an account.
* For personal API keys, deletes the sandbox for the key owner's account.
* Organization API keys may specify account_id to target any account
* within their organization.
* Deletes the sandbox for the key owner's account.
* Accounts with shared org membership may specify account_id to target another account.
*
* Authentication: x-api-key header or Authorization Bearer token required.
*
* Request body:
* - account_id: string (optional) - UUID of the account to delete for (org keys only)
* - account_id: string (optional) - UUID of the account to delete for (requires shared org membership or admin access)
*
* Response (200):
* - status: "success"
Expand Down
7 changes: 3 additions & 4 deletions app/api/sandboxes/setup/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,13 @@ export async function OPTIONS() {
*
* Triggers the setup-sandbox background task to create a personal sandbox,
* provision a GitHub repo, take a snapshot, and shut down.
* For personal API keys, sets up the sandbox for the key owner's account.
* Organization API keys may specify account_id to target any account
* within their organization.
* Sets up the sandbox for the key owner's account.
* Accounts with shared org membership may specify account_id to target another account.
*
* Authentication: x-api-key header or Authorization Bearer token required.
*
* Request body:
* - account_id: string (optional) - UUID of the account to set up for (org keys only)
* - account_id: string (optional) - UUID of the account to set up for (requires shared org membership or admin access)
*
* Response (200):
* - status: "success"
Expand Down
1 change: 1 addition & 0 deletions app/api/songs/analyze/presets/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export async function OPTIONS() {
* - status: "success"
* - presets: Array of { name, label, description, requiresAudio, responseFormat }
*
* @param request
* @returns A NextResponse with the list of available presets
*/
export async function GET(request: NextRequest): Promise<NextResponse> {
Expand Down
4 changes: 4 additions & 0 deletions app/api/transcribe/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@ import { NextRequest, NextResponse } from "next/server";
import { processAudioTranscription } from "@/lib/transcribe/processAudioTranscription";
import { formatTranscriptionError } from "@/lib/transcribe/types";

/**
*
* @param req
*/
export async function POST(req: NextRequest) {
try {
const body = await req.json();
Expand Down
2 changes: 1 addition & 1 deletion app/api/workspaces/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export async function OPTIONS() {
* Request body:
* - name (optional): The name of the workspace to create. Defaults to "Untitled".
* - account_id (optional): The ID of the account to create the workspace for (UUID).
* Only required for organization API keys creating workspaces on behalf of other accounts.
* Only required when creating workspaces on behalf of other accounts within a shared organization.
* - organization_id (optional): The organization ID to link the new workspace to (UUID).
* If provided, the workspace will appear in that organization's view.
* Access is validated to ensure the user has access to the organization.
Expand Down
4 changes: 2 additions & 2 deletions lib/accounts/validateOverrideAccountId.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ export type ValidateOverrideAccountIdResult = {
/**
* Validates that an API key has permission to override to a target accountId.
*
* Used when an org API key wants to create resources on behalf of another account.
* Checks that the API key belongs to an org with access to the target account.
* Used when an account wants to create resources on behalf of another account.
* Checks that the account has shared org membership with the target account.
*
* @param params.apiKey - The x-api-key header value
* @param params.targetAccountId - The accountId to override to
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ vi.mock("@/lib/admins/validateAdminAuth", () => ({
validateAdminAuth: vi.fn(),
}));

/**
*
* @param url
*/
function createMockRequest(url: string): NextRequest {
return {
url,
Expand Down
3 changes: 3 additions & 0 deletions lib/admins/privy/countNewAccounts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ import { getCutoffMs } from "./getCutoffMs";

/**
* Counts how many users in the list were created within the cutoff period.
*
* @param users
* @param period
*/
export function countNewAccounts(users: User[], period: PrivyLoginsPeriod): number {
const cutoffMs = getCutoffMs(period);
Expand Down
4 changes: 4 additions & 0 deletions lib/admins/privy/fetchPrivyLogins.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ export type FetchPrivyLoginsResult = {
totalPrivyUsers: number;
};

/**
*
* @param period
*/
export async function fetchPrivyLogins(period: PrivyLoginsPeriod): Promise<FetchPrivyLoginsResult> {
const isAll = period === "all";
const cutoffMs = getCutoffMs(period);
Expand Down
2 changes: 2 additions & 0 deletions lib/admins/privy/getCutoffMs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import { PERIOD_DAYS } from "./periodDays";
* Returns the cutoff timestamp in milliseconds for a given period.
* Uses midnight UTC calendar day boundaries to match Privy dashboard behavior.
* Returns 0 for "all" (no cutoff).
*
* @param period
*/
export function getCutoffMs(period: PrivyLoginsPeriod): number {
if (period === "all") return 0;
Expand Down
2 changes: 2 additions & 0 deletions lib/admins/privy/getLatestVerifiedAt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import type { User } from "@privy-io/node";
/**
* Returns the most recent latest_verified_at (in ms) across all linked_accounts for a Privy user.
* Returns null if no linked account has a latest_verified_at.
*
* @param user
*/
export function getLatestVerifiedAt(user: User): number | null {
const linkedAccounts = user.linked_accounts;
Expand Down
2 changes: 2 additions & 0 deletions lib/admins/privy/toMs.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
/**
* Normalizes a Privy timestamp to milliseconds.
* Privy docs say milliseconds but examples show seconds (10 digits).
*
* @param timestamp
*/
export function toMs(timestamp: number): number {
return timestamp > 1e12 ? timestamp : timestamp * 1000;
Expand Down
1 change: 1 addition & 0 deletions lib/ai/getModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { GatewayLanguageModelEntry } from "@ai-sdk/gateway";

/**
* Returns a specific model by its ID from the list of available models.
*
* @param modelId - The ID of the model to find
* @returns The matching model or undefined if not found
*/
Expand Down
2 changes: 2 additions & 0 deletions lib/ai/isEmbedModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { GatewayLanguageModelEntry } from "@ai-sdk/gateway";
/**
* Determines if a model is an embedding model (not suitable for chat).
* Embed models typically have 0 output pricing since they only produce embeddings.
*
* @param m
*/
export const isEmbedModel = (m: GatewayLanguageModelEntry): boolean => {
const pricing = m.pricing;
Expand Down
10 changes: 5 additions & 5 deletions lib/artists/__tests__/buildGetArtistsParams.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ describe("buildGetArtistsParams", () => {
vi.clearAllMocks();
});

it("returns auth accountId for personal key", async () => {
it("returns auth accountId for authenticated account", async () => {
const result = await buildGetArtistsParams({
accountId: "personal-account-123",
});
Expand All @@ -23,7 +23,7 @@ describe("buildGetArtistsParams", () => {
});
});

it("returns auth accountId for org key", async () => {
it("returns auth accountId for account with org membership", async () => {
const result = await buildGetArtistsParams({
accountId: "org-owner-123",
});
Expand Down Expand Up @@ -87,7 +87,7 @@ describe("buildGetArtistsParams", () => {
});
});

it("allows personal key to access targetAccountId via shared org", async () => {
it("allows account to access targetAccountId via shared org", async () => {
vi.mocked(canAccessAccount).mockResolvedValue(true);

const result = await buildGetArtistsParams({
Expand All @@ -105,7 +105,7 @@ describe("buildGetArtistsParams", () => {
});
});

it("returns error when personal key has no shared org with targetAccountId", async () => {
it("returns error when account has no shared org with targetAccountId", async () => {
vi.mocked(canAccessAccount).mockResolvedValue(false);

const result = await buildGetArtistsParams({
Expand All @@ -119,7 +119,7 @@ describe("buildGetArtistsParams", () => {
});
});

it("returns error when org key lacks access to targetAccountId", async () => {
it("returns error when account lacks access to targetAccountId", async () => {
vi.mocked(canAccessAccount).mockResolvedValue(false);

const result = await buildGetArtistsParams({
Expand Down
9 changes: 7 additions & 2 deletions lib/artists/__tests__/createArtistPostHandler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ vi.mock("@/lib/auth/validateAuthContext", () => ({
validateAuthContext: (...args: unknown[]) => mockValidateAuthContext(...args),
}));

/**
*
* @param body
* @param headers
*/
function createRequest(body: unknown, headers: Record<string, string> = {}): NextRequest {
const defaultHeaders: Record<string, string> = {
"Content-Type": "application/json",
Expand Down Expand Up @@ -60,7 +65,7 @@ describe("createArtistPostHandler", () => {
);
});

it("uses account_id override for org API keys", async () => {
it("uses account_id override for accounts with org access", async () => {
mockValidateAuthContext.mockResolvedValue({
accountId: "550e8400-e29b-41d4-a716-446655440000", // Overridden account
orgId: "org-account-id",
Expand Down Expand Up @@ -90,7 +95,7 @@ describe("createArtistPostHandler", () => {
expect(response.status).toBe(201);
});

it("returns 403 when org API key lacks access to account_id", async () => {
it("returns 403 when account lacks access to account_id", async () => {
mockValidateAuthContext.mockResolvedValue(
NextResponse.json(
{ status: "error", error: "Access denied to specified account_id" },
Expand Down
9 changes: 7 additions & 2 deletions lib/artists/__tests__/validateCreateArtistBody.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@ vi.mock("@/lib/auth/validateAuthContext", () => ({
validateAuthContext: (...args: unknown[]) => mockValidateAuthContext(...args),
}));

/**
*
* @param body
* @param headers
*/
function createRequest(body: unknown, headers: Record<string, string> = {}): NextRequest {
const defaultHeaders: Record<string, string> = { "Content-Type": "application/json" };
return new NextRequest("http://localhost/api/artists", {
Expand Down Expand Up @@ -63,7 +68,7 @@ describe("validateCreateArtistBody", () => {
}
});

it("uses account_id override for org API keys with access", async () => {
it("uses account_id override for accounts with org access", async () => {
mockValidateAuthContext.mockResolvedValue({
accountId: "550e8400-e29b-41d4-a716-446655440000", // Overridden account
orgId: "org-account-id",
Expand Down Expand Up @@ -109,7 +114,7 @@ describe("validateCreateArtistBody", () => {
}
});

it("returns 403 when org API key lacks access to account_id", async () => {
it("returns 403 when account lacks access to account_id", async () => {
mockValidateAuthContext.mockResolvedValue(
NextResponse.json(
{ status: "error", error: "Access denied to specified account_id" },
Expand Down
Loading
Loading